##Introduction This notebook leverages 20th Edition of Geolytix Open Supermarket Retail Points data set to perform some analytics on geospatial data.

The perceived task at hand assumes a large supermarket chain in the UK (Tesco) is interesting in acquiring a competitor chain, following which the challenge is to identify reasonable geographic regions into which to segment he stores, and most appropriate locations to position warehouse depots for stocking the stores.

The approach taken makes use of clustering techniques, as well as third part applications (run on Docker) to arrive at a solution.

The intention is to provide an example of how to work with geospatial data, in a way that could be applicable to a range of different industries - from logistics to marketing - and sectors - from retail to healthcare.

##Install Packages

#install.packages("leaflet")
#install.packages("tidyverse")
#install.packages("leaflet")
#install.packages("httr")
#install.packages("writexl")
#install.packages("readxl")
#install.packages("leaflet")
#install.packages("geosphere")
#install.packages("sf")
#install.packages("cluster")
#install.packages("factoextra")
#install.packages("dendextend")
#install.packages("dbscan")
#install.packages("curl")

library(httr)
library(readxl)
library(ggplot2)
library(tidyverse)
library(leaflet)
library(htmltools)
library(geosphere)
library(sf)
library(cluster)
library(factoextra)
library(dendextend)
library(dbscan)
library(curl)

Define Functions

# add leaflet title
overlayTitle <- function(plot, text) {
  
  tag.map.title <- tags$style(HTML("
  .leaflet-control.map-title { 
    transform: translate(-50%,20%);
    position: fixed !important;
    left: 50%;
    text-align: center;
    padding-left: 10px; 
    padding-right: 10px; 
    background: rgba(255,255,255,0.9);
    font-weight: bold;
    font-size: 18px;
  }
  "))
  
  title <- tags$div(
    tag.map.title, HTML(text)
  )
  plot %>% addControl(title, position = "topleft", className = "map-title")
  
}


# compute the number of locations within 500m
calcCloseStore <- function(master_stores, target_stores) {
  
  # convert both tables to sf standard
  master_stores_sf <- st_as_sf(master_stores, coords=c('lng', 'lat'), crs="epsg:4326")
  target_stores_sf <- st_as_sf(target_stores, coords=c('lng', 'lat'), crs="epsg:4326")
  
  # compute distance matrix
  # each row represents a target location and columns their distance to a master store (in meters)
  dist = st_distance(target_stores_sf, master_stores_sf)
  
  # convert to dataframe and compute the row min (we only care if it is within 500m not how often)
  dist <- data.frame(dist)
  dist$min <- apply(dist[], MARGIN =  1, FUN = min)
  
  # compute number of stores within 500m of a master brand store
  dist_close <- dist %>% 
    filter(min <=500)
  
  close_stores <- nrow(dist_close)
  
  return(close_stores)
}

Main Run

# load starting data
all_stores <- read_xls('data/GEOLYTIX - UK RetailPoints/uk_glx_open_retail_points_v24_202206.xls')

# plot store count by retailer
retailer_count <- all_stores %>%
  group_by(retailer) %>%
  tally()

ggplot(retailer_count, aes(x = reorder(retailer, -n), y = n)) +
  geom_bar(stat = "identity") + theme_minimal() +
  theme(axis.text.x = element_text(angle = 90, hjust = 1, vjust = 0.3))


# plot store count by cluster as %
retailer_count <- retailer_count %>%
  mutate(percentage = (n / sum(n))*100)

ggplot(retailer_count, aes(x = reorder(retailer, -percentage), y = percentage)) +
  geom_bar(stat = "identity") + theme_minimal() +
  theme(axis.text.x = element_text(angle = 90, hjust = 1, vjust = 0.3))

# create list of largest stores
majors <- c("Aldi", "Asda", "Lidl", "Marks and Spencer", "Morrisons", 
            "Sainsburys", "Tesco", "The Co-operative Group", "Waitrose")

# compute % of stores captured by major chains
print(retailer_count %>% filter(retailer %in% majors) %>% pull(percentage) %>% sum)
[1] 66.97561
# 66.975 %
# the major chains capture 2/3rds of the universe

# plot location of major chains
major_retailers <- all_stores %>% filter(`retailer` %in% majors) %>%
  select(id, retailer, postcode, lng = long_wgs, lat = lat_wgs, size_band)

pal <- colorFactor(
  palette = c(
    "cyan", "green", "purple", "black", "yellow", "orange", "blue", "turquoise", "red" 
    ),
  domain = major_retailers$retailer)

map <- leaflet(major_retailers) %>% addTiles() %>%
  addCircles(~lng, ~lat,color = ~pal(retailer)) %>%
  overlayTitle("Major Retailer Store Locations") %>%
  addLegend(position = "bottomright", values = ~retailer, pal = pal)

map

Purchase of Competitor

Assuming we are Tesco (as the largest supermarket) and are looking to acquire one of the other chains, we could need to set a series of conditions under which we would acquire a competitor. In this example, let’s imagine these are simply:

  1. Acquire based on which chain offers the “best coverage”- where there is minimum overlap with existing Tesco locations. i.e. find the competitor where least % of stores within a certain radius of Tesco stores

  2. Of the stores with minimal overlap in coverage, which have a profile of store locations that best matches Tesco’s business strategy

# we will set a cut-off of 500m each other - based on Great Circle distance
major_retailers <- major_retailers %>% 
  rowwise %>%
  mutate(coordinates = list(c(lng, lat))) %>%
  ungroup

# isolate the tesco locations
tesco_loc <- major_retailers %>%
  filter(retailer == "Tesco")

# obtain list of all other target brands
targets <- majors[majors != 'Tesco']

# iterate and compute % of each brand within 500m
for (brand in targets){
  
  target_loc <- major_retailers %>%
    filter(retailer == brand)
  
  close_stores <- calcCloseStore(tesco_loc, target_loc)
  
  print(brand)
  print((close_stores/nrow(target_loc))*100)
}
[1] "Aldi"
[1] 26.34298
[1] "Asda"
[1] 14.28571
[1] "Lidl"
[1] 29.41788
[1] "Marks and Spencer"
[1] 33.98876
[1] "Morrisons"
[1] 12.31964
[1] "Sainsburys"
[1] 33.30966
[1] "The Co-operative Group"
[1] 15.45048
[1] "Waitrose"
[1] 35.18006
# ------results
# "Aldi"
# 26.34298
# "Asda"
# 14.28571
# "Lidl"
# 29.41788
# "Marks and Spencer"
# 33.98876
# "Morrisons"
# 12.31964
# "Sainsburys"
# 33.30966
# "The Co-operative Group"
# 15.45048
# "Waitrose"
# 35.18006

From the above results we see that Morrisons and Asda have the lowest %. We can now explore the profile of their respective store portfolios (to see the number of stores these brands offer by size)

print(retailer_count %>% filter(retailer %in% c('Asda', 'Morrisons', 'The Co-operative Group')))
# Asda        637
# Morrisons   901
# Coop        2686

# as well as the array of store sizes - and how this compares to Tesco
store_size_counts <- all_stores %>%
  filter(retailer %in% c('Tesco', 'Asda', 'Morrisons', 'The Co-operative Group')) %>%
  group_by(retailer, size_band) %>%
  tally() %>%
  group_by(retailer) %>%
  mutate(percentage_stores = (n / sum(n))*100)


ggplot(store_size_counts, aes(x = size_band, y = percentage_stores, fill=retailer)) +
  geom_col(position = "dodge") + 
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 90, hjust = 1, vjust = 0.3))

From glancing at the charts, it is fairly obvious on first inspection that’s Morrisons portfolio profile is more similar to Tesco than Asda, but let’s prove it statistically.

For this, we can leverage the chi-squared test. The chi-squared is used to evaluate whether there is significant association between the categories of two categorical variables. In theory it is designed to assess whether the two variables are independent of one another.

However, in our case we can view the different supermarket brands as our variables, and we could look for the brands to have low levels of independence compared to the Tesco distribution - as lower independence is equal to higher similarity in our case

# convert to the correct structure
size_counts_wide <- store_size_counts %>%
  select(-n) %>%
  pivot_wider(names_from = retailer, values_from = percentage_stores) %>%
  replace(is.na(.), 0)

target_brands <- store_size_counts %>%
  select(retailer) %>%
  unique()

target_brands <- target_brands[target_brands != 'Tesco']

for (brand in target_brands){
  
  comparisons <- size_counts_wide %>%
    select(Tesco, brand)
  
  chisq <- chisq.test(comparisons)
  
  print(brand)
  print(chisq)
}
[1] "Asda"

    Pearson's Chi-squared test

data:  comparisons
X-squared = 88.809, df = 3, p-value < 2.2e-16

[1] "Morrisons"

    Pearson's Chi-squared test

data:  comparisons
X-squared = 13.187, df = 3, p-value = 0.004249

[1] "The Co-operative Group"

    Pearson's Chi-squared test

data:  comparisons
X-squared = 33.52, df = 3, p-value = 2.501e-07

From the above we want to keep in mind the null hypothesis - which is to say that as p-value tends towards 0, the interdependence between the two distributions (i.e. similarity in behaviour) becomes less. Theoretically if p>=0.05 then the variables are not interdependent.

In our case (whil not >= 0.05) we see that the p-value for Morrisons is orders of magnitude greater than for the other brands, meaning it has the most similar profile - as expected.

In fact, Asda has a distinctly different business model, with far greater focus (with >50% of their stores) on large superstores (over 2800m2). Meanwhile, the Co-operative operates a set of primarily small stores, with zero superstores in the group.

Given Morrisons has one of the lower % of overlap, and the most similar profile, we elect to “acquire” it

# create a combined dataset
combined_stores <- major_retailers %>%
  filter(retailer %in% c('Tesco', 'Morrisons'))

# now we have pretty good coverage across the UK
map <- leaflet(combined_stores) %>% addTiles() %>%
  addCircles(~lng, ~lat,color = 'blue', radius = 500, opacity = .5) %>%
  overlayTitle("Tesco Store Locations")

map

# save down combined dataset
write_rds(combined_stores, "data/combined_store.rds")

Identifying Which Stores to Close

Now we have a group of store locations, next we can optimise the portfolio we will do this by opting to close stores in the same location and then defining some logic to prioritise which stores to keep stores

# load checkpoint dataset
combined_stores <- read_rds("data/combined_store.rds")

# split by brand
tesco_stores <- combined_stores %>%
  filter(retailer == 'Tesco')

morrisons_stores <- combined_stores %>%
  filter(retailer == 'Morrisons')


# convert both tables to sf standard
tesco_stores_sf <- st_as_sf(tesco_stores, coords=c('lng', 'lat'), crs="epsg:4326")
morrisons_stores_sf <- st_as_sf(morrisons_stores, coords=c('lng', 'lat'), crs="epsg:4326")

# compute distance matrix
dist_sf = st_distance(tesco_stores_sf, morrisons_stores_sf)

# note: each row represents a Tesco location and columns their distance to a Morrisons store (in meters)
M <- as.matrix(dist_sf)
M <- unclass(M)

#create binary matrix to show where Tesco store (row) within 500m of Morrisons (column) 
M[] <- ifelse(M<500,1,0)

# convert to dataframe and add column to indicate Tesco index number
dist_sf <- data.frame(M)

dist_sf <- rownames_to_column(dist_sf) %>%
  rename(tesco_index = rowname)

# convert from wide-form to long-form
close_store_df <- dist_sf %>% 
  gather(key = morrisons_index, value = flag, -c(tesco_index)) %>%
  filter(flag == 1)

# clean up the morrions index column - removing the "X" from the naming convention
close_store_df <- close_store_df %>%
  rowwise() %>%
  mutate(morrisons_index = str_remove(morrisons_index, "X")) %>%
  ungroup

# check if we have any duplicates (i.e. one store close to two others)
nrow(close_store_df)
# 121
n_distinct(close_store_df$tesco_index)
# 117
n_distinct(close_store_df$morrisons_index)
# 111

# join back to the Tesco master to indicate Morrisons locations
tesco_stores_df <- rownames_to_column(tesco_stores) %>%
  rename(tesco_index = rowname) %>%
  inner_join(close_store_df, by='tesco_index')

# join on the relevant Morrisons store data
morrisons_stores_df <- rownames_to_column(morrisons_stores) %>%
  rename(morrisons_index = rowname)
  
store_pairs <- tesco_stores_df %>%
  left_join(morrisons_stores_df, by='morrisons_index')

Now we have found all the store pairs (i.e. those that are within 500m of each other) from across the Tesco vs Morrison’s portfolio. The next stage would be to decide the logic on which to keep stores.

In our case (and for ease of progressing with the rest of the project notebook), we will assume that we will simply remove the Morrison’s store, and keep the existing Tesco stores.

However, in practice you may wish to look at demographic (specifically household income, or population density) data by postcode, and make some assumptions around whether you want higher price goods (usually in smaller stores) in areas of higher density or income.

remove_ids <- store_pairs$id.y 

combined_cut <- combined_stores %>% 
  filter(!id %in% remove_ids)

nremove = nrow(combined_stores) - nrow(combined_cut)
print(paste0('number of stores removed: ', nremove))

write_rds(combined_cut, "data/combined_cut.rds")

Identifying Most Appropriate Warehouse Locations

After creating a combined portfolio “post acquisition”, we will decide where best to set up supply depots to stock the stores, which we will achieve by first grouping stores into sensible geographic regions - and then assign a depot to each region.

The method take will be to form clusters, based on geospatial location that capture the closest stores, while minimising overlap. The distribution warehouses/depots will then be located at the centroids of the clusters.

The aim is to split the locations into c.20 regions (and subsequently depots)

# load combined store set
master_stores <- read_rds("data/combined_cut.rds")

# take just the lat and lng coordinates
locs_df <- master_stores %>%
  select("id","lng", "lat") %>%
  column_to_rownames("id")

# set random seed to ensure repeatability of clustering
set.seed(123)

A few different clustering approaches were trialed as part of this work: - Partition-based clustering: KMeans - Density-based clustering: DBSCAN - Hierarchical clustering: Agglomerative

While KMeans produced good results, this method was discounted as it is bad practice to use with geospatial data. This is because it assumes coordinates are described in Euclidean coordinate system (which latitude and longitude are not). So while it may produce reasonable results for locations close together on Earth, it fails to account for any curvature when clustering locations that are far away

Meanwhile, DBSCAN (which looks to cluster based on variations in location denisty) produced strong clusters for most urban locations (e.g. big cities), but effective grouped all less dense areas into a few large clusters (which is ineffective for our use case)

Hierarchical clustering produced the best results - please skip to below code segment to see

Note: While code segments for both KMeans and DBSCAN are presented below, for interest only. To continue with the notebook, these can be skipped, and you can jump to the Hierarchical code below

KMEANS - FOR INTEREST ONLY

# Compute k-means with k = 20 (i.e. 20 regions)
km.res <- kmeans(locs_df, 20)

# assign the cluster numbers
locs_df_km<- locs_df %>% 
  mutate(clust = km.res$cluster)

# print
length(unique(locs_df_km$clust))
[1] 20
# 20

# plot outputs
pal <- colorFactor(
  palette = "RdYlBu",
  domain = locs_df_km$clust)

map <- leaflet(locs_df_km) %>% addProviderTiles(providers$CartoDB.Positron) %>%
  addCircles(~lng, ~lat,color = ~pal(clust)) %>%
  overlayTitle("Store Clusters") %>%
  addLegend(position = "bottomright", values = ~clust, pal = pal)
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
map


# plot store count by cluster
cluster_count_km <- locs_df_km %>%
  group_by(clust) %>%
  tally()

ggplot(cluster_count_km, aes(x = reorder(clust, -n), y = n)) +
  geom_bar(stat = "identity") + theme_minimal() +
  theme(axis.text.x = element_text(angle = 0, hjust = 0.5, vjust = 0.3))

DBSCAN - FOR INTEREST ONLY

clusters <- dbscan(locs_df, eps = 0.25, minPts = 70)[['cluster']]

length(unique(clusters))
[1] 10
# 20

locs_df_db <- locs_df %>%
  mutate(clust = clusters)

# plot outputs
pal <- colorFactor(
  palette = "RdYlBu",
  domain = locs_df_db$clust)

map <- leaflet(locs_df_db) %>% addProviderTiles(providers$CartoDB.Positron) %>%
  addCircles(~lng, ~lat,color = ~pal(clust)) %>%
  overlayTitle("Store Clusters") %>%
  addLegend(position = "bottomright", values = ~clust, pal = pal)

map

HIERARCHICAL - CHOSEN APPROACH

# compute distance matrix - between location pairs
locs_sf <- st_as_sf(locs_df, coords=c('lng', 'lat'), crs="epsg:4326")
dist_sf = st_distance(locs_sf, locs_sf)
mdist <- as.matrix(dist_sf)
mdist <- unclass(mdist)

# cluster based on distance to other locations
hc <- hclust(as.dist(mdist), method="complete")

# plot dendogram
plot(hc, cex = 0.6, hang = -1)


# cluster based on defined distance separation - trialed in order to get 20 clusters
d <- 215000
locs_df_hc <- locs_df %>%
  mutate(clust = cutree(hc, h=d))

# print
length(unique(locs_df_hc$clust))
[1] 20
# 20

# plot outputs
pal <- colorFactor(
  palette = "RdYlBu",
  domain = locs_df_hc$clust)

p <- leaflet(locs_df_hc) %>% addProviderTiles(providers$CartoDB.Positron)  %>%
  addCircles(~lng, ~lat,color = ~pal(clust)) %>%
  overlayTitle("Store Clusters") %>%
  addLegend(position = "bottomright", values = ~clust, pal = pal); p
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors

When viewing on a map, it appears to be an initially very sensible clustering. However, there looks to be some very small clusters, which it may not be logical to treat as a cluster in and of itself.

We explore this further by looking at the spread of store counts in each


# plot store count by cluster
cluster_count_hc <- locs_df_hc %>%
  group_by(clust) %>%
  tally()

ggplot(cluster_count_hc, aes(x = reorder(clust, -n), y = n)) +
  geom_bar(stat = "identity") + theme_minimal() +
  theme(axis.text.x = element_text(angle = 0, hjust = 0.5, vjust = 0.3))


# plot store count by cluster as %
cluster_count_hc <- cluster_count_hc %>%
  mutate(percentage = (n / nrow(locs_df_hc))*100)

ggplot(cluster_count_hc, aes(x = reorder(clust, -percentage), y = percentage)) +
  geom_bar(stat = "identity") + theme_minimal() +
  theme(axis.text.x = element_text(angle = 0, hjust = 0.5, vjust = 0.3))

From the above chart, it’s obvious that there are clusters with few store locations. Plotting these on the map (e.g. below code segment), shows that these tend to be in isolated locations - e.g. Shetland Islands, Hebrides, Orkney Islands, or Guernsey & Jersey

All of these have <1% of the total store locations (which is how we will decide to remove them)

# plot outputs - exploratory only
cluster_number <- 18

sample_cluster <- locs_df_hc %>%
  filter(clust == cluster_number)

map <- leaflet(sample_cluster) %>% addProviderTiles(providers$CartoDB.Positron)  %>%
  addCircles(~lng, ~lat,color = ~pal(clust)) %>%
  overlayTitle("Store Clusters") %>%
  addLegend(position = "bottomright", values = ~clust, pal = pal)
Warning: Some values were outside the color scale and will be treated as NAWarning: Some values were outside the color scale and will be treated as NAWarning: Some values were outside the color scale and will be treated as NA
map

We will remove such remote locations from our analysis and re-cluster

# remove those with less than 1%
remote_clusters <- cluster_count_hc %>%
  filter(percentage < 1) %>%
  select(clust)

locs_df_hc_cut <- locs_df_hc %>%
  filter(!clust %in% remote_clusters$clust)

# compute distance matrix - between location pairs
locs_sf <- st_as_sf(locs_df_hc_cut %>% select(lng, lat), coords=c('lng', 'lat'), crs="epsg:4326")
dist_sf = st_distance(locs_sf, locs_sf)
mdist <- as.matrix(dist_sf)
mdist <- unclass(mdist)

# plot dendogram
hc <- hclust(as.dist(mdist), method="complete")
plot(hc, cex = 0.6, hang = -1)


# cluster based on defined distance separation - trial and erro to get reasonable clusters
d <- 175000
locs_df_hc_cut <- locs_df_hc_cut %>%
  mutate(clust2 = cutree(hc, h=d))

# print
length(unique(locs_df_hc_cut$clust2))
[1] 20
# 20


# plot outputs
pal <- colorFactor(
  palette = "RdYlBu",
  domain = locs_df_hc_cut$clust2)

map <- leaflet(locs_df_hc_cut) %>% addProviderTiles(providers$CartoDB.Positron)  %>%
  addCircles(~lng, ~lat,color = ~pal(clust2)) %>%
  overlayTitle("Store Clusters") %>%
  addLegend(position = "bottomright", values = ~clust2, pal = pal)
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
map

Now that we have sensible clusters forming, the next step is to determine the location of the warehouses/depots that will service each region. For this, I will use simply the centroids - which can be computed using the mean lat,lng values

# compute the centre of each cluster and plot this
cluster_centroids <- locs_df_hc_cut %>%
  group_by(clust2) %>%
  summarize(mean_lng = mean(lng, na.rm=TRUE), mean_lat = mean(lat, na.rm=TRUE))


map <- leaflet(locs_df_hc_cut) %>% addProviderTiles(providers$CartoDB.Positron)  %>%
  addCircles(~lng, ~lat,color = ~pal(clust2)) %>%
  overlayTitle("Store Clusters") %>%
  addLegend(position = "bottomright", values = ~clust2, pal = pal) %>%
  addMarkers(data = cluster_centroids, ~mean_lng, ~mean_lat, label = ~as.character(clust2))
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
map

Now we have the warehouse locations, and some initial clusters. However, we will see that in some cases the hierarchical clustering does not result in sensible assignments. For example, there are situations where bodies of water exist that simply clustering by coordinates (lat, lng) will be unable to take account of.

e.g. in the below, we can see that a location is on the other side of the estuary

cluster_number <- 8

sample_cluster <- locs_df_hc_cut %>%
  filter(clust2 == cluster_number)

sample_centroid <- cluster_centroids %>%
  filter(clust2 == cluster_number)

map <- leaflet(sample_cluster) %>% addProviderTiles(providers$CartoDB.Positron)  %>%
  addCircles(~lng, ~lat,color = ~pal(clust)) %>%
  overlayTitle("Store Clusters") %>%
  addLegend(position = "bottomright", values = ~clust2, pal = pal) %>%
  addMarkers(data = sample_centroid, ~mean_lng, ~mean_lat)
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
map

In practice this means that the drive distance required from the depot to restock the store is far greater than initially anticipated. Furthermore, there may be a nearer depot (belonging to another cluster) that is better suited for restocking this store.

For this we will need to understand the drive time and reassign locations to their nearest cluster centre based on this.

# create a checkpoint
locs_df_hc_cut <- locs_df_hc_cut %>%
  select(-clust) %>%
  rename(cluster = clust2)

cluster_centroids <- cluster_centroids %>%
  rename(lat=mean_lat, lng=mean_lng, cluster = clust2)

write_rds(locs_df_hc_cut, "data/cut_store_locs.rds")
write_rds(cluster_centroids, "data/warehouse_locations.rds")

Optimise Based on Drive Time

For this part, we will leverage the brilliant Project OSRM (Open Source Routing Machine), which can be found here: https://github.com/Project-OSRM/osrm-backend

It will leverage docker and allow us to run a local app, which we can send API request to in order determine the drive distance and time between location pairs.

For further details on installation and running see the “install_osrm_docker.Rmd” file.

store_locations <- read_rds("data/cut_store_locs.rds")
warehouse_locations <- read_rds("data/warehouse_locations.rds")

# create the string of warehouse locations
coords <- c()
for (row in 1:nrow(warehouse_locations)){
  warehouse <- paste(warehouse_locations$lng[row], warehouse_locations$lat[row], sep=",")
  coords <- append(coords, warehouse)
  
}
warehouse_string <- paste(coords, collapse=";")

# iterate through each store and find the nearest warehouse/depot
cluster_vector <- c()
for (row in 1:nrow(store_locations)){
  location <- store_locations %>%
    select(lng, lat) %>%
    slice(row) %>%
    paste(collapse=",")
  
  response <- GET(paste0("http://127.0.0.1:5000/table/v1/driving/", location, ";", warehouse_string, "?sources=0"))
  result <- content(response, as='parsed')
  
  duration_matrix <- result$durations[[1]][-1] 
  
  nearest_warehouse <- warehouse_locations %>%
    mutate(store_travel_duration = as.numeric(duration_matrix)) %>%
    slice(which.min(.$store_travel_duration)) %>%
    pull(cluster)
  
  cluster_vector <- append(cluster_vector, nearest_warehouse)
  
  if (row%%100 == 0){
    print(paste0(row,"/",nrow(store_locations)))
  }
  
}

store_locations_updated <- store_locations %>%
  mutate(updated_cluster = cluster_vector) %>%
  rename(original_cluster = cluster) %>%
  mutate(same_cluster = case_when(
    original_cluster == updated_cluster ~ 1,
    original_cluster != updated_cluster ~ 0
    ))


write_rds(store_locations_updated, "data/master_store_df.rds")

Now we can re-plot the updated cluster assignments to check that previous issues have been addressed

store_locations_updated <- read_rds("data/master_store_df.rds")
warehouse_locations <- read_rds("data/warehouse_locations.rds")

# print number of changed locations
print(count(store_locations_updated, same_cluster, sort = TRUE))

# plot the new clusters
pal <- colorFactor(
  palette = "RdYlBu",
  domain = store_locations_updated$updated_cluster)

map <- leaflet(store_locations_updated) %>% addProviderTiles(providers$CartoDB.Positron)  %>%
  addCircles(~lng, ~lat,color = ~pal(updated_cluster), label = ~as.character(updated_cluster)) %>%
  overlayTitle("Store Clusters") %>%
  addLegend(position = "bottomright", values = ~updated_cluster, pal = pal) %>%
  addMarkers(data = warehouse_locations, ~lng, ~lat, label = ~as.character(cluster))
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
map

Assigning Primary vs Secondary Warehouses/Depots

The last thing we will do is to decide the appropriate mix of main/primary warehouses, and satellites/secondary ones. In practice it is often the case that a company operates several main distributions centres, which are then responsible for sending stock to smaller warehouses that service specific geographic regions.

We will replicate this approach by aiming for just 5 primary depots, with 15 secondary depots for servicing the remaining regions.

For this we will look at the store count assigned to each cluster (with the idea being to assign primary depots are those with most associated stores), and the geographic spread (to ensure our primary depots offer adequate coverage across the country).

This part is a slightly manual process.

cluster_count <- store_locations_updated %>%
  group_by(updated_cluster) %>%
  tally()

ggplot(cluster_count, aes(x = reorder(updated_cluster, -n), y = n)) +
  geom_bar(stat = "identity") + theme_minimal() +
  theme(axis.text.x = element_text(angle = 0, hjust = 0.5, vjust = 0.3))

top_7 <- c(12, 3, 11, 15, 1, 6, 5, 2, 16)

top_7_clusters <- store_locations_updated %>%
  filter(updated_cluster %in% top_7)

pal <- colorFactor(
  palette = "RdYlBu",
  domain = top_7_clusters$updated_cluster)

p <- leaflet(top_7_clusters) %>% addProviderTiles(providers$CartoDB.Positron)  %>%
  addCircles(~lng, ~lat,color = ~pal(updated_cluster)) %>%
  overlayTitle("Store Clusters") %>%
  addLegend(position = "bottomright", values = ~updated_cluster, pal = pal) %>%
  addMarkers(data = warehouse_locations, ~lng, ~lat, label = ~as.character(cluster)); p
  • London: Unsurprisingly London, with such a high population, captures the two largest clusters (3 & 12). Given the size of these clusters, we will take both as primary depots
  • Manchester: The third biggest cluster (11) is located in Manchester. Given the ability to service the second largest number of stores, as well as the norther regions, it is reasonable to also take this as a primary depot.
  • Birmingham: The decision for the next primary depot, becomes a weigh up between clusters 1 & 15. However, given the proximity of cluster 15 to Manchester, I chose to select location 1 as a primary depot, given it can be used to service service Wales and the Midlands, with the East of England being serviced out of Manchester or London as appropriate.
  • Glasgow: Lastly, it is also reasonable to assume a northern depot in Scotland to service the country, North of England, and Northern Ireland secondary depots. Glasgow (cluster 14) has the highest number of stores and is well located to service Northern Ireland on the west coast.

From manual investigation, and some business logic we have assigned our 5 primary depots. However, it is worth noting that this could be done using driving distance (e.g. pick 5 locations that minimise aggregated drive time to reach all secondary depots), or some alternative business logic. This would be an area of possible improvement.

# plot the final map
master_depots <- c(1, 3, 11, 12, 14)

warehouse_locations <- warehouse_locations %>%
  mutate(tier = ifelse(cluster %in% master_depots, 'master', 'sub')) %>%
  mutate(icon_colour = ifelse(tier == "master", "red", "blue"))

icons <- awesomeIcons(iconColor = "black",
                      library = "ion",
                      markerColor = warehouse_locations$icon_colour
                      )

pal <- colorFactor(
  palette = "RdYlBu",
  domain = store_locations_updated$updated_cluster)

map <- leaflet(store_locations_updated) %>% addProviderTiles(providers$CartoDB.Positron)  %>%
  addCircles(~lng, ~lat,color = ~pal(updated_cluster)) %>%
  overlayTitle("Store Clusters") %>%
  addLegend(position = "bottomright", values = ~updated_cluster, pal = pal) %>%
  addAwesomeMarkers(data = warehouse_locations, ~lng, ~lat, icon = icons, label = ~as.character(cluster))
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
map

Potential Improvements

Vehicle Routing:

The next step would be to determine the most optimal driving routes for trucks to leave each of the primary depots to service the secondary depots - i.e what is the most efficient method of trucks, leaving each of the the primary depots, to service the secondary depots in our network.

If we assume, given the size of deliveries, it is realistic to have a single truck per secondary depot restock, the problem becomes simply a shortest duration calculation from each secondary depot to the primary depots - which OSRM docker can handle.

However, if you want just one truck per depot to be responsible for restocking each of the associated secondary depots, then we need to frame the problem as a “travelling salesman problem” - i.e. what is the shortest route for a single truck to visit all the necessary stops just once and return home to the primary depot? For this, I would recommend exploring the VROOM Project (Vehicle Routing Open-Source Optimisation Machine): https://github.com/VROOM-Project

Primary Depot Selection:

As mentioned above, another potential improvement could be to select the location of the primary depots, based not on the number of locations and manual business logic, but based on which 5 locations minimise the drive time to each of the remaining secondary depots. For this we could use OSRM docker also.

Cluster Pruning:

While earlier on we disregarded clusters that were extremely remote, there are still a few locations contained in our clusters that are rather remote, with the cluster being fairly spread (e.g. North East Scotland).

To address this, we could iteratively remove locations from our dataset that are a certain distance from the nearest cluster centre, and then perform reclustering. The intention would be to repeatedly “prune” the most remote locations in between rounds of clustering.

This would result in more densely formed regions, but at the expense of excluding certain locations from our logistics network.

Closing Comments

There we have it, our clustered store territories, with the proposed location of each warehouse and a proposed view of which should me major depots, and which should be secondary depots.

While there are a few areas of potential improvement noted above, this project introduces and initial approach that can be taken to optimise logistics networks using geospatial data. The tools and techniques introduced can be used in their current form, or take further with mor complex approaches, to help companies gain actionable insights into their logistics, marketing, or acquisition approaches - and gain a competitive edge in their respective market.

LS0tDQp0aXRsZTogIlN1cGVybWFya2V0X1N0b3JlX0FuYWx5dGljcyINCmF1dGhvcjogInRvdG9nb3QiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQojI0ludHJvZHVjdGlvbg0KVGhpcyBub3RlYm9vayBsZXZlcmFnZXMgMjB0aCBFZGl0aW9uIG9mIEdlb2x5dGl4IE9wZW4gU3VwZXJtYXJrZXQgUmV0YWlsIFBvaW50cyBkYXRhIHNldCB0byBwZXJmb3JtIHNvbWUgYW5hbHl0aWNzIG9uIGdlb3NwYXRpYWwgZGF0YS4NCg0KVGhlIHBlcmNlaXZlZCB0YXNrIGF0IGhhbmQgYXNzdW1lcyBhIGxhcmdlIHN1cGVybWFya2V0IGNoYWluIGluIHRoZSBVSyAoVGVzY28pIGlzIGludGVyZXN0aW5nIGluIGFjcXVpcmluZyBhIGNvbXBldGl0b3IgY2hhaW4sIGZvbGxvd2luZyB3aGljaCB0aGUgY2hhbGxlbmdlIGlzIHRvIGlkZW50aWZ5IHJlYXNvbmFibGUgZ2VvZ3JhcGhpYyByZWdpb25zIGludG8gd2hpY2ggdG8gc2VnbWVudCBoZSBzdG9yZXMsIGFuZCBtb3N0IGFwcHJvcHJpYXRlIGxvY2F0aW9ucyB0byBwb3NpdGlvbiB3YXJlaG91c2UgZGVwb3RzIGZvciBzdG9ja2luZyB0aGUgc3RvcmVzLg0KDQpUaGUgYXBwcm9hY2ggdGFrZW4gbWFrZXMgdXNlIG9mIGNsdXN0ZXJpbmcgdGVjaG5pcXVlcywgYXMgd2VsbCBhcyB0aGlyZCBwYXJ0IGFwcGxpY2F0aW9ucyAocnVuIG9uIERvY2tlcikgdG8gYXJyaXZlIGF0IGEgc29sdXRpb24uDQoNClRoZSBpbnRlbnRpb24gaXMgdG8gcHJvdmlkZSBhbiBleGFtcGxlIG9mIGhvdyB0byB3b3JrIHdpdGggZ2Vvc3BhdGlhbCBkYXRhLCBpbiBhIHdheSB0aGF0IGNvdWxkIGJlIGFwcGxpY2FibGUgdG8gYSByYW5nZSBvZiBkaWZmZXJlbnQgaW5kdXN0cmllcyAtIGZyb20gbG9naXN0aWNzIHRvIG1hcmtldGluZyAtIGFuZCBzZWN0b3JzIC0gZnJvbSByZXRhaWwgdG8gaGVhbHRoY2FyZS4NCg0KDQoNCiMjSW5zdGFsbCBQYWNrYWdlcw0KYGBge3J9DQojaW5zdGFsbC5wYWNrYWdlcygibGVhZmxldCIpDQojaW5zdGFsbC5wYWNrYWdlcygidGlkeXZlcnNlIikNCiNpbnN0YWxsLnBhY2thZ2VzKCJsZWFmbGV0IikNCiNpbnN0YWxsLnBhY2thZ2VzKCJodHRyIikNCiNpbnN0YWxsLnBhY2thZ2VzKCJ3cml0ZXhsIikNCiNpbnN0YWxsLnBhY2thZ2VzKCJyZWFkeGwiKQ0KI2luc3RhbGwucGFja2FnZXMoImxlYWZsZXQiKQ0KI2luc3RhbGwucGFja2FnZXMoImdlb3NwaGVyZSIpDQojaW5zdGFsbC5wYWNrYWdlcygic2YiKQ0KI2luc3RhbGwucGFja2FnZXMoImNsdXN0ZXIiKQ0KI2luc3RhbGwucGFja2FnZXMoImZhY3RvZXh0cmEiKQ0KI2luc3RhbGwucGFja2FnZXMoImRlbmRleHRlbmQiKQ0KI2luc3RhbGwucGFja2FnZXMoImRic2NhbiIpDQojaW5zdGFsbC5wYWNrYWdlcygiY3VybCIpDQoNCmxpYnJhcnkoaHR0cikNCmxpYnJhcnkocmVhZHhsKQ0KbGlicmFyeShnZ3Bsb3QyKQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KGxlYWZsZXQpDQpsaWJyYXJ5KGh0bWx0b29scykNCmxpYnJhcnkoZ2Vvc3BoZXJlKQ0KbGlicmFyeShzZikNCmxpYnJhcnkoY2x1c3RlcikNCmxpYnJhcnkoZmFjdG9leHRyYSkNCmxpYnJhcnkoZGVuZGV4dGVuZCkNCmxpYnJhcnkoZGJzY2FuKQ0KbGlicmFyeShjdXJsKQ0KYGBgDQoNCg0KDQojIyBEZWZpbmUgRnVuY3Rpb25zDQpgYGB7cn0NCiMgYWRkIGxlYWZsZXQgdGl0bGUNCm92ZXJsYXlUaXRsZSA8LSBmdW5jdGlvbihwbG90LCB0ZXh0KSB7DQogIA0KICB0YWcubWFwLnRpdGxlIDwtIHRhZ3Mkc3R5bGUoSFRNTCgiDQogIC5sZWFmbGV0LWNvbnRyb2wubWFwLXRpdGxlIHsgDQogICAgdHJhbnNmb3JtOiB0cmFuc2xhdGUoLTUwJSwyMCUpOw0KICAgIHBvc2l0aW9uOiBmaXhlZCAhaW1wb3J0YW50Ow0KICAgIGxlZnQ6IDUwJTsNCiAgICB0ZXh0LWFsaWduOiBjZW50ZXI7DQogICAgcGFkZGluZy1sZWZ0OiAxMHB4OyANCiAgICBwYWRkaW5nLXJpZ2h0OiAxMHB4OyANCiAgICBiYWNrZ3JvdW5kOiByZ2JhKDI1NSwyNTUsMjU1LDAuOSk7DQogICAgZm9udC13ZWlnaHQ6IGJvbGQ7DQogICAgZm9udC1zaXplOiAxOHB4Ow0KICB9DQogICIpKQ0KICANCiAgdGl0bGUgPC0gdGFncyRkaXYoDQogICAgdGFnLm1hcC50aXRsZSwgSFRNTCh0ZXh0KQ0KICApDQogIHBsb3QgJT4lIGFkZENvbnRyb2wodGl0bGUsIHBvc2l0aW9uID0gInRvcGxlZnQiLCBjbGFzc05hbWUgPSAibWFwLXRpdGxlIikNCiAgDQp9DQoNCg0KIyBjb21wdXRlIHRoZSBudW1iZXIgb2YgbG9jYXRpb25zIHdpdGhpbiA1MDBtDQpjYWxjQ2xvc2VTdG9yZSA8LSBmdW5jdGlvbihtYXN0ZXJfc3RvcmVzLCB0YXJnZXRfc3RvcmVzKSB7DQogIA0KICAjIGNvbnZlcnQgYm90aCB0YWJsZXMgdG8gc2Ygc3RhbmRhcmQNCiAgbWFzdGVyX3N0b3Jlc19zZiA8LSBzdF9hc19zZihtYXN0ZXJfc3RvcmVzLCBjb29yZHM9YygnbG5nJywgJ2xhdCcpLCBjcnM9ImVwc2c6NDMyNiIpDQogIHRhcmdldF9zdG9yZXNfc2YgPC0gc3RfYXNfc2YodGFyZ2V0X3N0b3JlcywgY29vcmRzPWMoJ2xuZycsICdsYXQnKSwgY3JzPSJlcHNnOjQzMjYiKQ0KICANCiAgIyBjb21wdXRlIGRpc3RhbmNlIG1hdHJpeA0KICAjIGVhY2ggcm93IHJlcHJlc2VudHMgYSB0YXJnZXQgbG9jYXRpb24gYW5kIGNvbHVtbnMgdGhlaXIgZGlzdGFuY2UgdG8gYSBtYXN0ZXIgc3RvcmUgKGluIG1ldGVycykNCiAgZGlzdCA9IHN0X2Rpc3RhbmNlKHRhcmdldF9zdG9yZXNfc2YsIG1hc3Rlcl9zdG9yZXNfc2YpDQogIA0KICAjIGNvbnZlcnQgdG8gZGF0YWZyYW1lIGFuZCBjb21wdXRlIHRoZSByb3cgbWluICh3ZSBvbmx5IGNhcmUgaWYgaXQgaXMgd2l0aGluIDUwMG0gbm90IGhvdyBvZnRlbikNCiAgZGlzdCA8LSBkYXRhLmZyYW1lKGRpc3QpDQogIGRpc3QkbWluIDwtIGFwcGx5KGRpc3RbXSwgTUFSR0lOID0gIDEsIEZVTiA9IG1pbikNCiAgDQogICMgY29tcHV0ZSBudW1iZXIgb2Ygc3RvcmVzIHdpdGhpbiA1MDBtIG9mIGEgbWFzdGVyIGJyYW5kIHN0b3JlDQogIGRpc3RfY2xvc2UgPC0gZGlzdCAlPiUgDQogICAgZmlsdGVyKG1pbiA8PTUwMCkNCiAgDQogIGNsb3NlX3N0b3JlcyA8LSBucm93KGRpc3RfY2xvc2UpDQogIA0KICByZXR1cm4oY2xvc2Vfc3RvcmVzKQ0KfQ0KYGBgDQoNCg0KDQojIyBNYWluIFJ1bg0KYGBge3J9DQojIGxvYWQgc3RhcnRpbmcgZGF0YQ0KYWxsX3N0b3JlcyA8LSByZWFkX3hscygnZGF0YS9HRU9MWVRJWCAtIFVLIFJldGFpbFBvaW50cy91a19nbHhfb3Blbl9yZXRhaWxfcG9pbnRzX3YyNF8yMDIyMDYueGxzJykNCg0KIyBwbG90IHN0b3JlIGNvdW50IGJ5IHJldGFpbGVyDQpyZXRhaWxlcl9jb3VudCA8LSBhbGxfc3RvcmVzICU+JQ0KICBncm91cF9ieShyZXRhaWxlcikgJT4lDQogIHRhbGx5KCkNCg0KZ2dwbG90KHJldGFpbGVyX2NvdW50LCBhZXMoeCA9IHJlb3JkZXIocmV0YWlsZXIsIC1uKSwgeSA9IG4pKSArDQogIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArIHRoZW1lX21pbmltYWwoKSArDQogIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gOTAsIGhqdXN0ID0gMSwgdmp1c3QgPSAwLjMpKQ0KDQojIHBsb3Qgc3RvcmUgY291bnQgYnkgY2x1c3RlciBhcyAlDQpyZXRhaWxlcl9jb3VudCA8LSByZXRhaWxlcl9jb3VudCAlPiUNCiAgbXV0YXRlKHBlcmNlbnRhZ2UgPSAobiAvIHN1bShuKSkqMTAwKQ0KDQpnZ3Bsb3QocmV0YWlsZXJfY291bnQsIGFlcyh4ID0gcmVvcmRlcihyZXRhaWxlciwgLXBlcmNlbnRhZ2UpLCB5ID0gcGVyY2VudGFnZSkpICsNCiAgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpICsgdGhlbWVfbWluaW1hbCgpICsNCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSA5MCwgaGp1c3QgPSAxLCB2anVzdCA9IDAuMykpDQpgYGANCg0KYGBge3J9DQojIGNyZWF0ZSBsaXN0IG9mIGxhcmdlc3Qgc3RvcmVzDQptYWpvcnMgPC0gYygiQWxkaSIsICJBc2RhIiwgIkxpZGwiLCAiTWFya3MgYW5kIFNwZW5jZXIiLCAiTW9ycmlzb25zIiwgDQogICAgICAgICAgICAiU2FpbnNidXJ5cyIsICJUZXNjbyIsICJUaGUgQ28tb3BlcmF0aXZlIEdyb3VwIiwgIldhaXRyb3NlIikNCg0KIyBjb21wdXRlICUgb2Ygc3RvcmVzIGNhcHR1cmVkIGJ5IG1ham9yIGNoYWlucw0KcHJpbnQocmV0YWlsZXJfY291bnQgJT4lIGZpbHRlcihyZXRhaWxlciAlaW4lIG1ham9ycykgJT4lIHB1bGwocGVyY2VudGFnZSkgJT4lIHN1bSkNCiMgNjYuOTc1ICUNCiMgdGhlIG1ham9yIGNoYWlucyBjYXB0dXJlIDIvM3JkcyBvZiB0aGUgdW5pdmVyc2UNCg0KIyBwbG90IGxvY2F0aW9uIG9mIG1ham9yIGNoYWlucw0KbWFqb3JfcmV0YWlsZXJzIDwtIGFsbF9zdG9yZXMgJT4lIGZpbHRlcihgcmV0YWlsZXJgICVpbiUgbWFqb3JzKSAlPiUNCiAgc2VsZWN0KGlkLCByZXRhaWxlciwgcG9zdGNvZGUsIGxuZyA9IGxvbmdfd2dzLCBsYXQgPSBsYXRfd2dzLCBzaXplX2JhbmQpDQoNCnBhbCA8LSBjb2xvckZhY3RvcigNCiAgcGFsZXR0ZSA9IGMoDQogICAgImN5YW4iLCAiZ3JlZW4iLCAicHVycGxlIiwgImJsYWNrIiwgInllbGxvdyIsICJvcmFuZ2UiLCAiYmx1ZSIsICJ0dXJxdW9pc2UiLCAicmVkIiANCiAgICApLA0KICBkb21haW4gPSBtYWpvcl9yZXRhaWxlcnMkcmV0YWlsZXIpDQoNCm1hcCA8LSBsZWFmbGV0KG1ham9yX3JldGFpbGVycykgJT4lIGFkZFRpbGVzKCkgJT4lDQogIGFkZENpcmNsZXMofmxuZywgfmxhdCxjb2xvciA9IH5wYWwocmV0YWlsZXIpKSAlPiUNCiAgb3ZlcmxheVRpdGxlKCJNYWpvciBSZXRhaWxlciBTdG9yZSBMb2NhdGlvbnMiKSAlPiUNCiAgYWRkTGVnZW5kKHBvc2l0aW9uID0gImJvdHRvbXJpZ2h0IiwgdmFsdWVzID0gfnJldGFpbGVyLCBwYWwgPSBwYWwpDQoNCm1hcA0KYGBgDQoNCiMjIyBQdXJjaGFzZSBvZiBDb21wZXRpdG9yDQoNCkFzc3VtaW5nIHdlIGFyZSBUZXNjbyAoYXMgdGhlIGxhcmdlc3Qgc3VwZXJtYXJrZXQpIGFuZCBhcmUgbG9va2luZyB0byBhY3F1aXJlIG9uZSBvZiB0aGUgb3RoZXIgY2hhaW5zLCB3ZSBjb3VsZCBuZWVkIHRvIHNldCBhIHNlcmllcyBvZiBjb25kaXRpb25zIHVuZGVyIHdoaWNoIHdlIHdvdWxkIGFjcXVpcmUgYSBjb21wZXRpdG9yLiBJbiB0aGlzIGV4YW1wbGUsIGxldCdzIGltYWdpbmUgdGhlc2UgYXJlIHNpbXBseToNCg0KMS4gQWNxdWlyZSBiYXNlZCBvbiB3aGljaCBjaGFpbiBvZmZlcnMgdGhlICJiZXN0IGNvdmVyYWdlIi0gd2hlcmUgdGhlcmUgaXMgbWluaW11bSBvdmVybGFwIHdpdGggZXhpc3RpbmcgVGVzY28gbG9jYXRpb25zLiBpLmUuIGZpbmQgdGhlIGNvbXBldGl0b3Igd2hlcmUgbGVhc3QgJSBvZiBzdG9yZXMgd2l0aGluIGEgY2VydGFpbiByYWRpdXMgb2YgVGVzY28gc3RvcmVzDQoNCjIuIE9mIHRoZSBzdG9yZXMgd2l0aCBtaW5pbWFsIG92ZXJsYXAgaW4gY292ZXJhZ2UsIHdoaWNoIGhhdmUgYSBwcm9maWxlIG9mIHN0b3JlIGxvY2F0aW9ucyB0aGF0IGJlc3QgbWF0Y2hlcyBUZXNjbydzIGJ1c2luZXNzIHN0cmF0ZWd5DQpgYGB7cn0NCiMgd2Ugd2lsbCBzZXQgYSBjdXQtb2ZmIG9mIDUwMG0gZWFjaCBvdGhlciAtIGJhc2VkIG9uIEdyZWF0IENpcmNsZSBkaXN0YW5jZQ0KbWFqb3JfcmV0YWlsZXJzIDwtIG1ham9yX3JldGFpbGVycyAlPiUgDQogIHJvd3dpc2UgJT4lDQogIG11dGF0ZShjb29yZGluYXRlcyA9IGxpc3QoYyhsbmcsIGxhdCkpKSAlPiUNCiAgdW5ncm91cA0KDQojIGlzb2xhdGUgdGhlIHRlc2NvIGxvY2F0aW9ucw0KdGVzY29fbG9jIDwtIG1ham9yX3JldGFpbGVycyAlPiUNCiAgZmlsdGVyKHJldGFpbGVyID09ICJUZXNjbyIpDQoNCiMgb2J0YWluIGxpc3Qgb2YgYWxsIG90aGVyIHRhcmdldCBicmFuZHMNCnRhcmdldHMgPC0gbWFqb3JzW21ham9ycyAhPSAnVGVzY28nXQ0KDQojIGl0ZXJhdGUgYW5kIGNvbXB1dGUgJSBvZiBlYWNoIGJyYW5kIHdpdGhpbiA1MDBtDQpmb3IgKGJyYW5kIGluIHRhcmdldHMpew0KICANCiAgdGFyZ2V0X2xvYyA8LSBtYWpvcl9yZXRhaWxlcnMgJT4lDQogICAgZmlsdGVyKHJldGFpbGVyID09IGJyYW5kKQ0KICANCiAgY2xvc2Vfc3RvcmVzIDwtIGNhbGNDbG9zZVN0b3JlKHRlc2NvX2xvYywgdGFyZ2V0X2xvYykNCiAgDQogIHByaW50KGJyYW5kKQ0KICBwcmludCgoY2xvc2Vfc3RvcmVzL25yb3codGFyZ2V0X2xvYykpKjEwMCkNCn0NCiMgLS0tLS0tcmVzdWx0cw0KIyAiQWxkaSINCiMgMjYuMzQyOTgNCiMgIkFzZGEiDQojIDE0LjI4NTcxDQojICJMaWRsIg0KIyAyOS40MTc4OA0KIyAiTWFya3MgYW5kIFNwZW5jZXIiDQojIDMzLjk4ODc2DQojICJNb3JyaXNvbnMiDQojIDEyLjMxOTY0DQojICJTYWluc2J1cnlzIg0KIyAzMy4zMDk2Ng0KIyAiVGhlIENvLW9wZXJhdGl2ZSBHcm91cCINCiMgMTUuNDUwNDgNCiMgIldhaXRyb3NlIg0KIyAzNS4xODAwNg0KYGBgDQoNCkZyb20gdGhlIGFib3ZlIHJlc3VsdHMgd2Ugc2VlIHRoYXQgTW9ycmlzb25zIGFuZCBBc2RhIGhhdmUgdGhlIGxvd2VzdCAlLiBXZSBjYW4gbm93IGV4cGxvcmUgdGhlIHByb2ZpbGUgb2YgdGhlaXIgcmVzcGVjdGl2ZSBzdG9yZSBwb3J0Zm9saW9zICh0byBzZWUgdGhlIG51bWJlciBvZiBzdG9yZXMgdGhlc2UgYnJhbmRzIG9mZmVyIGJ5IHNpemUpDQpgYGB7cn0NCnByaW50KHJldGFpbGVyX2NvdW50ICU+JSBmaWx0ZXIocmV0YWlsZXIgJWluJSBjKCdBc2RhJywgJ01vcnJpc29ucycsICdUaGUgQ28tb3BlcmF0aXZlIEdyb3VwJykpKQ0KIyBBc2RhICAgICAgICA2MzcNCiMgTW9ycmlzb25zICAgOTAxDQojIENvb3AgICAgICAgIDI2ODYNCg0KIyBhcyB3ZWxsIGFzIHRoZSBhcnJheSBvZiBzdG9yZSBzaXplcyAtIGFuZCBob3cgdGhpcyBjb21wYXJlcyB0byBUZXNjbw0Kc3RvcmVfc2l6ZV9jb3VudHMgPC0gYWxsX3N0b3JlcyAlPiUNCiAgZmlsdGVyKHJldGFpbGVyICVpbiUgYygnVGVzY28nLCAnQXNkYScsICdNb3JyaXNvbnMnLCAnVGhlIENvLW9wZXJhdGl2ZSBHcm91cCcpKSAlPiUNCiAgZ3JvdXBfYnkocmV0YWlsZXIsIHNpemVfYmFuZCkgJT4lDQogIHRhbGx5KCkgJT4lDQogIGdyb3VwX2J5KHJldGFpbGVyKSAlPiUNCiAgbXV0YXRlKHBlcmNlbnRhZ2Vfc3RvcmVzID0gKG4gLyBzdW0obikpKjEwMCkNCg0KDQpnZ3Bsb3Qoc3RvcmVfc2l6ZV9jb3VudHMsIGFlcyh4ID0gc2l6ZV9iYW5kLCB5ID0gcGVyY2VudGFnZV9zdG9yZXMsIGZpbGw9cmV0YWlsZXIpKSArDQogIGdlb21fY29sKHBvc2l0aW9uID0gImRvZGdlIikgKyANCiAgdGhlbWVfbWluaW1hbCgpICsNCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSA5MCwgaGp1c3QgPSAxLCB2anVzdCA9IDAuMykpDQpgYGANCkZyb20gZ2xhbmNpbmcgYXQgdGhlIGNoYXJ0cywgaXQgaXMgZmFpcmx5IG9idmlvdXMgb24gZmlyc3QgaW5zcGVjdGlvbiB0aGF0J3MgTW9ycmlzb25zIHBvcnRmb2xpbyBwcm9maWxlIGlzIG1vcmUgc2ltaWxhciB0byBUZXNjbyB0aGFuIEFzZGEsIGJ1dCBsZXQncyBwcm92ZSBpdCBzdGF0aXN0aWNhbGx5Lg0KDQpGb3IgdGhpcywgd2UgY2FuIGxldmVyYWdlIHRoZSBjaGktc3F1YXJlZCB0ZXN0LiBUaGUgY2hpLXNxdWFyZWQgaXMgdXNlZCB0byBldmFsdWF0ZSB3aGV0aGVyIHRoZXJlIGlzIHNpZ25pZmljYW50IGFzc29jaWF0aW9uIGJldHdlZW4gdGhlIGNhdGVnb3JpZXMgb2YgdHdvIGNhdGVnb3JpY2FsIHZhcmlhYmxlcy4gSW4gdGhlb3J5IGl0IGlzIGRlc2lnbmVkIHRvIGFzc2VzcyB3aGV0aGVyIHRoZSB0d28gdmFyaWFibGVzIGFyZSBpbmRlcGVuZGVudCBvZiBvbmUgYW5vdGhlci4NCg0KSG93ZXZlciwgaW4gb3VyIGNhc2Ugd2UgY2FuIHZpZXcgdGhlIGRpZmZlcmVudCBzdXBlcm1hcmtldCBicmFuZHMgYXMgb3VyIHZhcmlhYmxlcywgYW5kIHdlIGNvdWxkIGxvb2sgZm9yIHRoZSBicmFuZHMgdG8gaGF2ZSBsb3cgbGV2ZWxzIG9mIGluZGVwZW5kZW5jZSBjb21wYXJlZCB0byB0aGUgVGVzY28gZGlzdHJpYnV0aW9uIC0gYXMgbG93ZXIgaW5kZXBlbmRlbmNlIGlzIGVxdWFsIHRvIGhpZ2hlciBzaW1pbGFyaXR5IGluIG91ciBjYXNlDQoNCmBgYHtyfQ0KIyBjb252ZXJ0IHRvIHRoZSBjb3JyZWN0IHN0cnVjdHVyZQ0Kc2l6ZV9jb3VudHNfd2lkZSA8LSBzdG9yZV9zaXplX2NvdW50cyAlPiUNCiAgc2VsZWN0KC1uKSAlPiUNCiAgcGl2b3Rfd2lkZXIobmFtZXNfZnJvbSA9IHJldGFpbGVyLCB2YWx1ZXNfZnJvbSA9IHBlcmNlbnRhZ2Vfc3RvcmVzKSAlPiUNCiAgcmVwbGFjZShpcy5uYSguKSwgMCkNCg0KdGFyZ2V0X2JyYW5kcyA8LSBzdG9yZV9zaXplX2NvdW50cyAlPiUNCiAgc2VsZWN0KHJldGFpbGVyKSAlPiUNCiAgdW5pcXVlKCkNCg0KdGFyZ2V0X2JyYW5kcyA8LSB0YXJnZXRfYnJhbmRzW3RhcmdldF9icmFuZHMgIT0gJ1Rlc2NvJ10NCg0KZm9yIChicmFuZCBpbiB0YXJnZXRfYnJhbmRzKXsNCiAgDQogIGNvbXBhcmlzb25zIDwtIHNpemVfY291bnRzX3dpZGUgJT4lDQogICAgc2VsZWN0KFRlc2NvLCBicmFuZCkNCiAgDQogIGNoaXNxIDwtIGNoaXNxLnRlc3QoY29tcGFyaXNvbnMpDQogIA0KICBwcmludChicmFuZCkNCiAgcHJpbnQoY2hpc3EpDQp9DQpgYGANCkZyb20gdGhlIGFib3ZlIHdlIHdhbnQgdG8ga2VlcCBpbiBtaW5kIHRoZSBudWxsIGh5cG90aGVzaXMgLSB3aGljaCBpcyB0byBzYXkgdGhhdCBhcyBwLXZhbHVlIHRlbmRzIHRvd2FyZHMgMCwgdGhlIGludGVyZGVwZW5kZW5jZSBiZXR3ZWVuIHRoZSB0d28gZGlzdHJpYnV0aW9ucyAoaS5lLiBzaW1pbGFyaXR5IGluIGJlaGF2aW91cikgYmVjb21lcyBsZXNzLiBUaGVvcmV0aWNhbGx5IGlmIHA+PTAuMDUgdGhlbiB0aGUgdmFyaWFibGVzIGFyZSBub3QgaW50ZXJkZXBlbmRlbnQuDQoNCkluIG91ciBjYXNlICh3aGlsIG5vdCA+PSAwLjA1KSB3ZSBzZWUgdGhhdCB0aGUgcC12YWx1ZSBmb3IgTW9ycmlzb25zIGlzIG9yZGVycyBvZiBtYWduaXR1ZGUgZ3JlYXRlciB0aGFuIGZvciB0aGUgb3RoZXIgYnJhbmRzLCBtZWFuaW5nIGl0IGhhcyB0aGUgbW9zdCBzaW1pbGFyIHByb2ZpbGUgLSBhcyBleHBlY3RlZC4gDQoNCkluIGZhY3QsIEFzZGEgaGFzIGEgZGlzdGluY3RseSBkaWZmZXJlbnQgYnVzaW5lc3MgbW9kZWwsIHdpdGggZmFyIGdyZWF0ZXIgZm9jdXMgKHdpdGggPjUwJSBvZiB0aGVpciBzdG9yZXMpIG9uIGxhcmdlIHN1cGVyc3RvcmVzIChvdmVyIDI4MDBtMikuIE1lYW53aGlsZSwgdGhlIENvLW9wZXJhdGl2ZSBvcGVyYXRlcyBhIHNldCBvZiBwcmltYXJpbHkgc21hbGwgc3RvcmVzLCB3aXRoIHplcm8gc3VwZXJzdG9yZXMgaW4gdGhlIGdyb3VwLg0KDQpHaXZlbiBNb3JyaXNvbnMgaGFzIG9uZSBvZiB0aGUgbG93ZXIgJSBvZiBvdmVybGFwLCBhbmQgdGhlIG1vc3Qgc2ltaWxhciBwcm9maWxlLCB3ZSBlbGVjdCB0byAiYWNxdWlyZSIgaXQNCg0KYGBge3J9DQojIGNyZWF0ZSBhIGNvbWJpbmVkIGRhdGFzZXQNCmNvbWJpbmVkX3N0b3JlcyA8LSBtYWpvcl9yZXRhaWxlcnMgJT4lDQogIGZpbHRlcihyZXRhaWxlciAlaW4lIGMoJ1Rlc2NvJywgJ01vcnJpc29ucycpKQ0KDQojIG5vdyB3ZSBoYXZlIHByZXR0eSBnb29kIGNvdmVyYWdlIGFjcm9zcyB0aGUgVUsNCm1hcCA8LSBsZWFmbGV0KGNvbWJpbmVkX3N0b3JlcykgJT4lIGFkZFRpbGVzKCkgJT4lDQogIGFkZENpcmNsZXMofmxuZywgfmxhdCxjb2xvciA9ICdibHVlJywgcmFkaXVzID0gNTAwLCBvcGFjaXR5ID0gLjUpICU+JQ0KICBvdmVybGF5VGl0bGUoIlRlc2NvIFN0b3JlIExvY2F0aW9ucyIpDQoNCm1hcA0KDQojIHNhdmUgZG93biBjb21iaW5lZCBkYXRhc2V0DQp3cml0ZV9yZHMoY29tYmluZWRfc3RvcmVzLCAiZGF0YS9jb21iaW5lZF9zdG9yZS5yZHMiKQ0KYGBgDQoNCg0KDQojIyMgSWRlbnRpZnlpbmcgV2hpY2ggU3RvcmVzIHRvIENsb3NlDQoNCk5vdyB3ZSBoYXZlIGEgZ3JvdXAgb2Ygc3RvcmUgbG9jYXRpb25zLCBuZXh0IHdlIGNhbiBvcHRpbWlzZSB0aGUgcG9ydGZvbGlvIHdlIHdpbGwgZG8gdGhpcyBieSBvcHRpbmcgdG8gY2xvc2Ugc3RvcmVzIGluIHRoZSBzYW1lIGxvY2F0aW9uIGFuZCB0aGVuIGRlZmluaW5nIHNvbWUgbG9naWMgdG8gcHJpb3JpdGlzZSB3aGljaCBzdG9yZXMgdG8ga2VlcCBzdG9yZXMNCg0KDQpgYGB7cn0NCiMgbG9hZCBjaGVja3BvaW50IGRhdGFzZXQNCmNvbWJpbmVkX3N0b3JlcyA8LSByZWFkX3JkcygiZGF0YS9jb21iaW5lZF9zdG9yZS5yZHMiKQ0KDQojIHNwbGl0IGJ5IGJyYW5kDQp0ZXNjb19zdG9yZXMgPC0gY29tYmluZWRfc3RvcmVzICU+JQ0KICBmaWx0ZXIocmV0YWlsZXIgPT0gJ1Rlc2NvJykNCg0KbW9ycmlzb25zX3N0b3JlcyA8LSBjb21iaW5lZF9zdG9yZXMgJT4lDQogIGZpbHRlcihyZXRhaWxlciA9PSAnTW9ycmlzb25zJykNCg0KDQojIGNvbnZlcnQgYm90aCB0YWJsZXMgdG8gc2Ygc3RhbmRhcmQNCnRlc2NvX3N0b3Jlc19zZiA8LSBzdF9hc19zZih0ZXNjb19zdG9yZXMsIGNvb3Jkcz1jKCdsbmcnLCAnbGF0JyksIGNycz0iZXBzZzo0MzI2IikNCm1vcnJpc29uc19zdG9yZXNfc2YgPC0gc3RfYXNfc2YobW9ycmlzb25zX3N0b3JlcywgY29vcmRzPWMoJ2xuZycsICdsYXQnKSwgY3JzPSJlcHNnOjQzMjYiKQ0KDQojIGNvbXB1dGUgZGlzdGFuY2UgbWF0cml4DQpkaXN0X3NmID0gc3RfZGlzdGFuY2UodGVzY29fc3RvcmVzX3NmLCBtb3JyaXNvbnNfc3RvcmVzX3NmKQ0KDQojIG5vdGU6IGVhY2ggcm93IHJlcHJlc2VudHMgYSBUZXNjbyBsb2NhdGlvbiBhbmQgY29sdW1ucyB0aGVpciBkaXN0YW5jZSB0byBhIE1vcnJpc29ucyBzdG9yZSAoaW4gbWV0ZXJzKQ0KTSA8LSBhcy5tYXRyaXgoZGlzdF9zZikNCk0gPC0gdW5jbGFzcyhNKQ0KDQojY3JlYXRlIGJpbmFyeSBtYXRyaXggdG8gc2hvdyB3aGVyZSBUZXNjbyBzdG9yZSAocm93KSB3aXRoaW4gNTAwbSBvZiBNb3JyaXNvbnMgKGNvbHVtbikgDQpNW10gPC0gaWZlbHNlKE08NTAwLDEsMCkNCg0KIyBjb252ZXJ0IHRvIGRhdGFmcmFtZSBhbmQgYWRkIGNvbHVtbiB0byBpbmRpY2F0ZSBUZXNjbyBpbmRleCBudW1iZXINCmRpc3Rfc2YgPC0gZGF0YS5mcmFtZShNKQ0KDQpkaXN0X3NmIDwtIHJvd25hbWVzX3RvX2NvbHVtbihkaXN0X3NmKSAlPiUNCiAgcmVuYW1lKHRlc2NvX2luZGV4ID0gcm93bmFtZSkNCg0KIyBjb252ZXJ0IGZyb20gd2lkZS1mb3JtIHRvIGxvbmctZm9ybQ0KY2xvc2Vfc3RvcmVfZGYgPC0gZGlzdF9zZiAlPiUgDQogIGdhdGhlcihrZXkgPSBtb3JyaXNvbnNfaW5kZXgsIHZhbHVlID0gZmxhZywgLWModGVzY29faW5kZXgpKSAlPiUNCiAgZmlsdGVyKGZsYWcgPT0gMSkNCg0KIyBjbGVhbiB1cCB0aGUgbW9ycmlvbnMgaW5kZXggY29sdW1uIC0gcmVtb3ZpbmcgdGhlICJYIiBmcm9tIHRoZSBuYW1pbmcgY29udmVudGlvbg0KY2xvc2Vfc3RvcmVfZGYgPC0gY2xvc2Vfc3RvcmVfZGYgJT4lDQogIHJvd3dpc2UoKSAlPiUNCiAgbXV0YXRlKG1vcnJpc29uc19pbmRleCA9IHN0cl9yZW1vdmUobW9ycmlzb25zX2luZGV4LCAiWCIpKSAlPiUNCiAgdW5ncm91cA0KDQojIGNoZWNrIGlmIHdlIGhhdmUgYW55IGR1cGxpY2F0ZXMgKGkuZS4gb25lIHN0b3JlIGNsb3NlIHRvIHR3byBvdGhlcnMpDQpucm93KGNsb3NlX3N0b3JlX2RmKQ0KIyAxMjENCm5fZGlzdGluY3QoY2xvc2Vfc3RvcmVfZGYkdGVzY29faW5kZXgpDQojIDExNw0Kbl9kaXN0aW5jdChjbG9zZV9zdG9yZV9kZiRtb3JyaXNvbnNfaW5kZXgpDQojIDExMQ0KDQojIGpvaW4gYmFjayB0byB0aGUgVGVzY28gbWFzdGVyIHRvIGluZGljYXRlIE1vcnJpc29ucyBsb2NhdGlvbnMNCnRlc2NvX3N0b3Jlc19kZiA8LSByb3duYW1lc190b19jb2x1bW4odGVzY29fc3RvcmVzKSAlPiUNCiAgcmVuYW1lKHRlc2NvX2luZGV4ID0gcm93bmFtZSkgJT4lDQogIGlubmVyX2pvaW4oY2xvc2Vfc3RvcmVfZGYsIGJ5PSd0ZXNjb19pbmRleCcpDQoNCiMgam9pbiBvbiB0aGUgcmVsZXZhbnQgTW9ycmlzb25zIHN0b3JlIGRhdGENCm1vcnJpc29uc19zdG9yZXNfZGYgPC0gcm93bmFtZXNfdG9fY29sdW1uKG1vcnJpc29uc19zdG9yZXMpICU+JQ0KICByZW5hbWUobW9ycmlzb25zX2luZGV4ID0gcm93bmFtZSkNCiAgDQpzdG9yZV9wYWlycyA8LSB0ZXNjb19zdG9yZXNfZGYgJT4lDQogIGxlZnRfam9pbihtb3JyaXNvbnNfc3RvcmVzX2RmLCBieT0nbW9ycmlzb25zX2luZGV4JykNCmBgYA0KTm93IHdlIGhhdmUgZm91bmQgYWxsIHRoZSBzdG9yZSBwYWlycyAoaS5lLiB0aG9zZSB0aGF0IGFyZSB3aXRoaW4gNTAwbSBvZiBlYWNoIG90aGVyKSBmcm9tIGFjcm9zcyB0aGUgVGVzY28gdnMgTW9ycmlzb24ncyBwb3J0Zm9saW8uIFRoZSBuZXh0IHN0YWdlIHdvdWxkIGJlIHRvIGRlY2lkZSB0aGUgbG9naWMgb24gd2hpY2ggdG8ga2VlcCBzdG9yZXMuDQoNCkluIG91ciBjYXNlIChhbmQgZm9yIGVhc2Ugb2YgcHJvZ3Jlc3Npbmcgd2l0aCB0aGUgcmVzdCBvZiB0aGUgcHJvamVjdCBub3RlYm9vayksIHdlIHdpbGwgYXNzdW1lIHRoYXQgd2Ugd2lsbCBzaW1wbHkgcmVtb3ZlIHRoZSBNb3JyaXNvbidzIHN0b3JlLCBhbmQga2VlcCB0aGUgZXhpc3RpbmcgVGVzY28gc3RvcmVzLg0KDQpIb3dldmVyLCBpbiBwcmFjdGljZSB5b3UgbWF5IHdpc2ggdG8gbG9vayBhdCBkZW1vZ3JhcGhpYyAoc3BlY2lmaWNhbGx5IGhvdXNlaG9sZCBpbmNvbWUsIG9yIHBvcHVsYXRpb24gZGVuc2l0eSkgZGF0YSBieSBwb3N0Y29kZSwgYW5kIG1ha2Ugc29tZSBhc3N1bXB0aW9ucyBhcm91bmQgd2hldGhlciB5b3Ugd2FudCBoaWdoZXIgcHJpY2UgZ29vZHMgKHVzdWFsbHkgaW4gc21hbGxlciBzdG9yZXMpIGluIGFyZWFzIG9mIGhpZ2hlciBkZW5zaXR5IG9yIGluY29tZS4gDQoNCmBgYHtyfQ0KcmVtb3ZlX2lkcyA8LSBzdG9yZV9wYWlycyRpZC55IA0KDQpjb21iaW5lZF9jdXQgPC0gY29tYmluZWRfc3RvcmVzICU+JSANCiAgZmlsdGVyKCFpZCAlaW4lIHJlbW92ZV9pZHMpDQoNCm5yZW1vdmUgPSBucm93KGNvbWJpbmVkX3N0b3JlcykgLSBucm93KGNvbWJpbmVkX2N1dCkNCnByaW50KHBhc3RlMCgnbnVtYmVyIG9mIHN0b3JlcyByZW1vdmVkOiAnLCBucmVtb3ZlKSkNCg0Kd3JpdGVfcmRzKGNvbWJpbmVkX2N1dCwgImRhdGEvY29tYmluZWRfY3V0LnJkcyIpDQpgYGANCg0KDQoNCiMjIyBJZGVudGlmeWluZyBNb3N0IEFwcHJvcHJpYXRlIFdhcmVob3VzZSBMb2NhdGlvbnMNCg0KQWZ0ZXIgY3JlYXRpbmcgYSBjb21iaW5lZCBwb3J0Zm9saW8gInBvc3QgYWNxdWlzaXRpb24iLCB3ZSB3aWxsIGRlY2lkZSB3aGVyZSBiZXN0IHRvIHNldCB1cCBzdXBwbHkgZGVwb3RzIHRvIHN0b2NrIHRoZSBzdG9yZXMsIHdoaWNoIHdlIHdpbGwgYWNoaWV2ZSBieSBmaXJzdCBncm91cGluZyBzdG9yZXMgaW50byBzZW5zaWJsZSBnZW9ncmFwaGljIHJlZ2lvbnMgLSBhbmQgdGhlbiBhc3NpZ24gYSBkZXBvdCB0byBlYWNoIHJlZ2lvbi4gDQoNClRoZSBtZXRob2QgdGFrZSB3aWxsIGJlIHRvIGZvcm0gY2x1c3RlcnMsIGJhc2VkIG9uIGdlb3NwYXRpYWwgbG9jYXRpb24gdGhhdCBjYXB0dXJlIHRoZSBjbG9zZXN0IHN0b3Jlcywgd2hpbGUgbWluaW1pc2luZyBvdmVybGFwLiBUaGUgZGlzdHJpYnV0aW9uIHdhcmVob3VzZXMvZGVwb3RzIHdpbGwgdGhlbiBiZSBsb2NhdGVkIGF0IHRoZSBjZW50cm9pZHMgb2YgdGhlIGNsdXN0ZXJzLg0KDQpUaGUgYWltIGlzIHRvIHNwbGl0IHRoZSBsb2NhdGlvbnMgaW50byBjLjIwIHJlZ2lvbnMgKGFuZCBzdWJzZXF1ZW50bHkgZGVwb3RzKQ0KDQpgYGB7cn0NCiMgbG9hZCBjb21iaW5lZCBzdG9yZSBzZXQNCm1hc3Rlcl9zdG9yZXMgPC0gcmVhZF9yZHMoImRhdGEvY29tYmluZWRfY3V0LnJkcyIpDQoNCiMgdGFrZSBqdXN0IHRoZSBsYXQgYW5kIGxuZyBjb29yZGluYXRlcw0KbG9jc19kZiA8LSBtYXN0ZXJfc3RvcmVzICU+JQ0KICBzZWxlY3QoImlkIiwibG5nIiwgImxhdCIpICU+JQ0KICBjb2x1bW5fdG9fcm93bmFtZXMoImlkIikNCg0KIyBzZXQgcmFuZG9tIHNlZWQgdG8gZW5zdXJlIHJlcGVhdGFiaWxpdHkgb2YgY2x1c3RlcmluZw0Kc2V0LnNlZWQoMTIzKQ0KYGBgDQoNCg0KQSBmZXcgZGlmZmVyZW50IGNsdXN0ZXJpbmcgYXBwcm9hY2hlcyB3ZXJlIHRyaWFsZWQgYXMgcGFydCBvZiB0aGlzIHdvcms6DQotIFBhcnRpdGlvbi1iYXNlZCBjbHVzdGVyaW5nOiBLTWVhbnMNCi0gRGVuc2l0eS1iYXNlZCBjbHVzdGVyaW5nOiBEQlNDQU4NCi0gSGllcmFyY2hpY2FsIGNsdXN0ZXJpbmc6IEFnZ2xvbWVyYXRpdmUNCg0KV2hpbGUgS01lYW5zIHByb2R1Y2VkIGdvb2QgcmVzdWx0cywgdGhpcyBtZXRob2Qgd2FzIGRpc2NvdW50ZWQgYXMgaXQgaXMgYmFkIHByYWN0aWNlIHRvIHVzZSB3aXRoIGdlb3NwYXRpYWwgZGF0YS4gVGhpcyBpcyBiZWNhdXNlIGl0IGFzc3VtZXMgY29vcmRpbmF0ZXMgYXJlIGRlc2NyaWJlZCBpbiBFdWNsaWRlYW4gY29vcmRpbmF0ZSBzeXN0ZW0gKHdoaWNoIGxhdGl0dWRlIGFuZCBsb25naXR1ZGUgYXJlIG5vdCkuIFNvIHdoaWxlIGl0IG1heSBwcm9kdWNlIHJlYXNvbmFibGUgcmVzdWx0cyBmb3IgbG9jYXRpb25zIGNsb3NlIHRvZ2V0aGVyIG9uIEVhcnRoLCBpdCBmYWlscyB0byBhY2NvdW50IGZvciBhbnkgY3VydmF0dXJlIHdoZW4gY2x1c3RlcmluZyBsb2NhdGlvbnMgdGhhdCBhcmUgZmFyIGF3YXkNCg0KTWVhbndoaWxlLCBEQlNDQU4gKHdoaWNoIGxvb2tzIHRvIGNsdXN0ZXIgYmFzZWQgb24gdmFyaWF0aW9ucyBpbiBsb2NhdGlvbiBkZW5pc3R5KSBwcm9kdWNlZCBzdHJvbmcgY2x1c3RlcnMgZm9yIG1vc3QgdXJiYW4gbG9jYXRpb25zIChlLmcuIGJpZyBjaXRpZXMpLCBidXQgZWZmZWN0aXZlIGdyb3VwZWQgYWxsIGxlc3MgZGVuc2UgYXJlYXMgaW50byBhIGZldyBsYXJnZSBjbHVzdGVycyAod2hpY2ggaXMgaW5lZmZlY3RpdmUgZm9yIG91ciB1c2UgY2FzZSkNCg0KSGllcmFyY2hpY2FsIGNsdXN0ZXJpbmcgcHJvZHVjZWQgdGhlIGJlc3QgcmVzdWx0cyAtIHBsZWFzZSBza2lwIHRvIGJlbG93IGNvZGUgc2VnbWVudCB0byBzZWUNCg0KTm90ZTogV2hpbGUgY29kZSBzZWdtZW50cyBmb3IgYm90aCBLTWVhbnMgYW5kIERCU0NBTiBhcmUgcHJlc2VudGVkIGJlbG93LCBmb3IgaW50ZXJlc3Qgb25seS4gVG8gY29udGludWUgd2l0aCB0aGUgbm90ZWJvb2ssIHRoZXNlIGNhbiBiZSBza2lwcGVkLCBhbmQgeW91IGNhbiBqdW1wIHRvIHRoZSBIaWVyYXJjaGljYWwgY29kZSBiZWxvdw0KDQoNCktNRUFOUyAtIEZPUiBJTlRFUkVTVCBPTkxZDQpgYGB7cn0NCiMgQ29tcHV0ZSBrLW1lYW5zIHdpdGggayA9IDIwIChpLmUuIDIwIHJlZ2lvbnMpDQprbS5yZXMgPC0ga21lYW5zKGxvY3NfZGYsIDIwKQ0KDQojIGFzc2lnbiB0aGUgY2x1c3RlciBudW1iZXJzDQpsb2NzX2RmX2ttPC0gbG9jc19kZiAlPiUgDQogIG11dGF0ZShjbHVzdCA9IGttLnJlcyRjbHVzdGVyKQ0KDQojIHByaW50DQpsZW5ndGgodW5pcXVlKGxvY3NfZGZfa20kY2x1c3QpKQ0KIyAyMA0KDQojIHBsb3Qgb3V0cHV0cw0KcGFsIDwtIGNvbG9yRmFjdG9yKA0KICBwYWxldHRlID0gIlJkWWxCdSIsDQogIGRvbWFpbiA9IGxvY3NfZGZfa20kY2x1c3QpDQoNCm1hcCA8LSBsZWFmbGV0KGxvY3NfZGZfa20pICU+JSBhZGRQcm92aWRlclRpbGVzKHByb3ZpZGVycyRDYXJ0b0RCLlBvc2l0cm9uKSAlPiUNCiAgYWRkQ2lyY2xlcyh+bG5nLCB+bGF0LGNvbG9yID0gfnBhbChjbHVzdCkpICU+JQ0KICBvdmVybGF5VGl0bGUoIlN0b3JlIENsdXN0ZXJzIikgJT4lDQogIGFkZExlZ2VuZChwb3NpdGlvbiA9ICJib3R0b21yaWdodCIsIHZhbHVlcyA9IH5jbHVzdCwgcGFsID0gcGFsKQ0KDQptYXANCg0KDQojIHBsb3Qgc3RvcmUgY291bnQgYnkgY2x1c3Rlcg0KY2x1c3Rlcl9jb3VudF9rbSA8LSBsb2NzX2RmX2ttICU+JQ0KICBncm91cF9ieShjbHVzdCkgJT4lDQogIHRhbGx5KCkNCg0KZ2dwbG90KGNsdXN0ZXJfY291bnRfa20sIGFlcyh4ID0gcmVvcmRlcihjbHVzdCwgLW4pLCB5ID0gbikpICsNCiAgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpICsgdGhlbWVfbWluaW1hbCgpICsNCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSAwLCBoanVzdCA9IDAuNSwgdmp1c3QgPSAwLjMpKQ0KDQpgYGANCg0KREJTQ0FOIC0gRk9SIElOVEVSRVNUIE9OTFkNCmBgYHtyfQ0KY2x1c3RlcnMgPC0gZGJzY2FuKGxvY3NfZGYsIGVwcyA9IDAuMjUsIG1pblB0cyA9IDcwKVtbJ2NsdXN0ZXInXV0NCg0KbGVuZ3RoKHVuaXF1ZShjbHVzdGVycykpDQojIDIwDQoNCmxvY3NfZGZfZGIgPC0gbG9jc19kZiAlPiUNCiAgbXV0YXRlKGNsdXN0ID0gY2x1c3RlcnMpDQoNCiMgcGxvdCBvdXRwdXRzDQpwYWwgPC0gY29sb3JGYWN0b3IoDQogIHBhbGV0dGUgPSAiUmRZbEJ1IiwNCiAgZG9tYWluID0gbG9jc19kZl9kYiRjbHVzdCkNCg0KbWFwIDwtIGxlYWZsZXQobG9jc19kZl9kYikgJT4lIGFkZFByb3ZpZGVyVGlsZXMocHJvdmlkZXJzJENhcnRvREIuUG9zaXRyb24pICU+JQ0KICBhZGRDaXJjbGVzKH5sbmcsIH5sYXQsY29sb3IgPSB+cGFsKGNsdXN0KSkgJT4lDQogIG92ZXJsYXlUaXRsZSgiU3RvcmUgQ2x1c3RlcnMiKSAlPiUNCiAgYWRkTGVnZW5kKHBvc2l0aW9uID0gImJvdHRvbXJpZ2h0IiwgdmFsdWVzID0gfmNsdXN0LCBwYWwgPSBwYWwpDQoNCm1hcA0KYGBgDQoNCg0KDQpISUVSQVJDSElDQUwgLSBDSE9TRU4gQVBQUk9BQ0gNCmBgYHtyfQ0KIyBjb21wdXRlIGRpc3RhbmNlIG1hdHJpeCAtIGJldHdlZW4gbG9jYXRpb24gcGFpcnMNCmxvY3Nfc2YgPC0gc3RfYXNfc2YobG9jc19kZiwgY29vcmRzPWMoJ2xuZycsICdsYXQnKSwgY3JzPSJlcHNnOjQzMjYiKQ0KZGlzdF9zZiA9IHN0X2Rpc3RhbmNlKGxvY3Nfc2YsIGxvY3Nfc2YpDQptZGlzdCA8LSBhcy5tYXRyaXgoZGlzdF9zZikNCm1kaXN0IDwtIHVuY2xhc3MobWRpc3QpDQoNCiMgY2x1c3RlciBiYXNlZCBvbiBkaXN0YW5jZSB0byBvdGhlciBsb2NhdGlvbnMNCmhjIDwtIGhjbHVzdChhcy5kaXN0KG1kaXN0KSwgbWV0aG9kPSJjb21wbGV0ZSIpDQoNCiMgcGxvdCBkZW5kb2dyYW0NCnBsb3QoaGMsIGNleCA9IDAuNiwgaGFuZyA9IC0xKQ0KDQojIGNsdXN0ZXIgYmFzZWQgb24gZGVmaW5lZCBkaXN0YW5jZSBzZXBhcmF0aW9uIC0gdHJpYWxlZCBpbiBvcmRlciB0byBnZXQgMjAgY2x1c3RlcnMNCmQgPC0gMjE1MDAwDQpsb2NzX2RmX2hjIDwtIGxvY3NfZGYgJT4lDQogIG11dGF0ZShjbHVzdCA9IGN1dHJlZShoYywgaD1kKSkNCg0KIyBwcmludA0KbGVuZ3RoKHVuaXF1ZShsb2NzX2RmX2hjJGNsdXN0KSkNCiMgMjANCg0KIyBwbG90IG91dHB1dHMNCnBhbCA8LSBjb2xvckZhY3RvcigNCiAgcGFsZXR0ZSA9ICJSZFlsQnUiLA0KICBkb21haW4gPSBsb2NzX2RmX2hjJGNsdXN0KQ0KDQptYXAgPC0gbGVhZmxldChsb2NzX2RmX2hjKSAlPiUgYWRkUHJvdmlkZXJUaWxlcyhwcm92aWRlcnMkQ2FydG9EQi5Qb3NpdHJvbikgICU+JQ0KICBhZGRDaXJjbGVzKH5sbmcsIH5sYXQsY29sb3IgPSB+cGFsKGNsdXN0KSkgJT4lDQogIG92ZXJsYXlUaXRsZSgiU3RvcmUgQ2x1c3RlcnMiKSAlPiUNCiAgYWRkTGVnZW5kKHBvc2l0aW9uID0gImJvdHRvbXJpZ2h0IiwgdmFsdWVzID0gfmNsdXN0LCBwYWwgPSBwYWwpDQoNCm1hcA0KYGBgDQoNCldoZW4gdmlld2luZyBvbiBhIG1hcCwgaXQgYXBwZWFycyB0byBiZSBhbiBpbml0aWFsbHkgdmVyeSBzZW5zaWJsZSBjbHVzdGVyaW5nLiBIb3dldmVyLCB0aGVyZSBsb29rcyB0byBiZSBzb21lIHZlcnkgc21hbGwgY2x1c3RlcnMsIHdoaWNoIGl0IG1heSBub3QgYmUgbG9naWNhbCB0byB0cmVhdCBhcyBhIGNsdXN0ZXIgaW4gYW5kIG9mIGl0c2VsZi4NCg0KV2UgZXhwbG9yZSB0aGlzIGZ1cnRoZXIgYnkgbG9va2luZyBhdCB0aGUgc3ByZWFkIG9mIHN0b3JlIGNvdW50cyBpbiBlYWNoDQoNCmBgYHtyfQ0KDQojIHBsb3Qgc3RvcmUgY291bnQgYnkgY2x1c3Rlcg0KY2x1c3Rlcl9jb3VudF9oYyA8LSBsb2NzX2RmX2hjICU+JQ0KICBncm91cF9ieShjbHVzdCkgJT4lDQogIHRhbGx5KCkNCg0KZ2dwbG90KGNsdXN0ZXJfY291bnRfaGMsIGFlcyh4ID0gcmVvcmRlcihjbHVzdCwgLW4pLCB5ID0gbikpICsNCiAgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpICsgdGhlbWVfbWluaW1hbCgpICsNCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSAwLCBoanVzdCA9IDAuNSwgdmp1c3QgPSAwLjMpKQ0KDQojIHBsb3Qgc3RvcmUgY291bnQgYnkgY2x1c3RlciBhcyAlDQpjbHVzdGVyX2NvdW50X2hjIDwtIGNsdXN0ZXJfY291bnRfaGMgJT4lDQogIG11dGF0ZShwZXJjZW50YWdlID0gKG4gLyBucm93KGxvY3NfZGZfaGMpKSoxMDApDQoNCmdncGxvdChjbHVzdGVyX2NvdW50X2hjLCBhZXMoeCA9IHJlb3JkZXIoY2x1c3QsIC1wZXJjZW50YWdlKSwgeSA9IHBlcmNlbnRhZ2UpKSArDQogIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArIHRoZW1lX21pbmltYWwoKSArDQogIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gMCwgaGp1c3QgPSAwLjUsIHZqdXN0ID0gMC4zKSkNCmBgYA0KDQpGcm9tIHRoZSBhYm92ZSBjaGFydCwgaXQncyBvYnZpb3VzIHRoYXQgdGhlcmUgYXJlIGNsdXN0ZXJzIHdpdGggZmV3IHN0b3JlIGxvY2F0aW9ucy4gUGxvdHRpbmcgdGhlc2Ugb24gdGhlIG1hcCAoZS5nLiBiZWxvdyBjb2RlIHNlZ21lbnQpLCBzaG93cyB0aGF0IHRoZXNlIHRlbmQgdG8gYmUgaW4gaXNvbGF0ZWQgbG9jYXRpb25zIC0gZS5nLiBTaGV0bGFuZCBJc2xhbmRzLCBIZWJyaWRlcywgT3JrbmV5IElzbGFuZHMsIG9yIEd1ZXJuc2V5ICYgSmVyc2V5DQoNCkFsbCBvZiB0aGVzZSBoYXZlIDwxJSBvZiB0aGUgdG90YWwgc3RvcmUgbG9jYXRpb25zICh3aGljaCBpcyBob3cgd2Ugd2lsbCBkZWNpZGUgdG8gcmVtb3ZlIHRoZW0pDQpgYGB7cn0NCiMgcGxvdCBvdXRwdXRzIC0gZXhwbG9yYXRvcnkgb25seQ0KY2x1c3Rlcl9udW1iZXIgPC0gMTgNCg0Kc2FtcGxlX2NsdXN0ZXIgPC0gbG9jc19kZl9oYyAlPiUNCiAgZmlsdGVyKGNsdXN0ID09IGNsdXN0ZXJfbnVtYmVyKQ0KDQptYXAgPC0gbGVhZmxldChzYW1wbGVfY2x1c3RlcikgJT4lIGFkZFByb3ZpZGVyVGlsZXMocHJvdmlkZXJzJENhcnRvREIuUG9zaXRyb24pICAlPiUNCiAgYWRkQ2lyY2xlcyh+bG5nLCB+bGF0LGNvbG9yID0gfnBhbChjbHVzdCkpICU+JQ0KICBvdmVybGF5VGl0bGUoIlN0b3JlIENsdXN0ZXJzIikgJT4lDQogIGFkZExlZ2VuZChwb3NpdGlvbiA9ICJib3R0b21yaWdodCIsIHZhbHVlcyA9IH5jbHVzdCwgcGFsID0gcGFsKQ0KDQptYXANCmBgYA0KDQoNCldlIHdpbGwgcmVtb3ZlIHN1Y2ggcmVtb3RlIGxvY2F0aW9ucyBmcm9tIG91ciBhbmFseXNpcyBhbmQgcmUtY2x1c3Rlcg0KYGBge3J9DQojIHJlbW92ZSB0aG9zZSB3aXRoIGxlc3MgdGhhbiAxJQ0KcmVtb3RlX2NsdXN0ZXJzIDwtIGNsdXN0ZXJfY291bnRfaGMgJT4lDQogIGZpbHRlcihwZXJjZW50YWdlIDwgMSkgJT4lDQogIHNlbGVjdChjbHVzdCkNCg0KbG9jc19kZl9oY19jdXQgPC0gbG9jc19kZl9oYyAlPiUNCiAgZmlsdGVyKCFjbHVzdCAlaW4lIHJlbW90ZV9jbHVzdGVycyRjbHVzdCkNCg0KIyBjb21wdXRlIGRpc3RhbmNlIG1hdHJpeCAtIGJldHdlZW4gbG9jYXRpb24gcGFpcnMNCmxvY3Nfc2YgPC0gc3RfYXNfc2YobG9jc19kZl9oY19jdXQgJT4lIHNlbGVjdChsbmcsIGxhdCksIGNvb3Jkcz1jKCdsbmcnLCAnbGF0JyksIGNycz0iZXBzZzo0MzI2IikNCmRpc3Rfc2YgPSBzdF9kaXN0YW5jZShsb2NzX3NmLCBsb2NzX3NmKQ0KbWRpc3QgPC0gYXMubWF0cml4KGRpc3Rfc2YpDQptZGlzdCA8LSB1bmNsYXNzKG1kaXN0KQ0KDQojIHBsb3QgZGVuZG9ncmFtDQpoYyA8LSBoY2x1c3QoYXMuZGlzdChtZGlzdCksIG1ldGhvZD0iY29tcGxldGUiKQ0KcGxvdChoYywgY2V4ID0gMC42LCBoYW5nID0gLTEpDQoNCiMgY2x1c3RlciBiYXNlZCBvbiBkZWZpbmVkIGRpc3RhbmNlIHNlcGFyYXRpb24gLSB0cmlhbCBhbmQgZXJybyB0byBnZXQgcmVhc29uYWJsZSBjbHVzdGVycw0KZCA8LSAxNzUwMDANCmxvY3NfZGZfaGNfY3V0IDwtIGxvY3NfZGZfaGNfY3V0ICU+JQ0KICBtdXRhdGUoY2x1c3QyID0gY3V0cmVlKGhjLCBoPWQpKQ0KDQojIHByaW50DQpsZW5ndGgodW5pcXVlKGxvY3NfZGZfaGNfY3V0JGNsdXN0MikpDQojIDIwDQoNCg0KIyBwbG90IG91dHB1dHMNCnBhbCA8LSBjb2xvckZhY3RvcigNCiAgcGFsZXR0ZSA9ICJSZFlsQnUiLA0KICBkb21haW4gPSBsb2NzX2RmX2hjX2N1dCRjbHVzdDIpDQoNCm1hcCA8LSBsZWFmbGV0KGxvY3NfZGZfaGNfY3V0KSAlPiUgYWRkUHJvdmlkZXJUaWxlcyhwcm92aWRlcnMkQ2FydG9EQi5Qb3NpdHJvbikgICU+JQ0KICBhZGRDaXJjbGVzKH5sbmcsIH5sYXQsY29sb3IgPSB+cGFsKGNsdXN0MikpICU+JQ0KICBvdmVybGF5VGl0bGUoIlN0b3JlIENsdXN0ZXJzIikgJT4lDQogIGFkZExlZ2VuZChwb3NpdGlvbiA9ICJib3R0b21yaWdodCIsIHZhbHVlcyA9IH5jbHVzdDIsIHBhbCA9IHBhbCkNCg0KbWFwDQpgYGANCg0KTm93IHRoYXQgd2UgaGF2ZSBzZW5zaWJsZSBjbHVzdGVycyBmb3JtaW5nLCB0aGUgbmV4dCBzdGVwIGlzIHRvIGRldGVybWluZSB0aGUgbG9jYXRpb24gb2YgdGhlIHdhcmVob3VzZXMvZGVwb3RzIHRoYXQgd2lsbCBzZXJ2aWNlIGVhY2ggcmVnaW9uLiBGb3IgdGhpcywgSSB3aWxsIHVzZSBzaW1wbHkgdGhlIGNlbnRyb2lkcyAtIHdoaWNoIGNhbiBiZSBjb21wdXRlZCB1c2luZyB0aGUgbWVhbiBsYXQsbG5nIHZhbHVlcw0KDQpgYGB7cn0NCiMgY29tcHV0ZSB0aGUgY2VudHJlIG9mIGVhY2ggY2x1c3RlciBhbmQgcGxvdCB0aGlzDQpjbHVzdGVyX2NlbnRyb2lkcyA8LSBsb2NzX2RmX2hjX2N1dCAlPiUNCiAgZ3JvdXBfYnkoY2x1c3QyKSAlPiUNCiAgc3VtbWFyaXplKG1lYW5fbG5nID0gbWVhbihsbmcsIG5hLnJtPVRSVUUpLCBtZWFuX2xhdCA9IG1lYW4obGF0LCBuYS5ybT1UUlVFKSkNCg0KDQptYXAgPC0gbGVhZmxldChsb2NzX2RmX2hjX2N1dCkgJT4lIGFkZFByb3ZpZGVyVGlsZXMocHJvdmlkZXJzJENhcnRvREIuUG9zaXRyb24pICAlPiUNCiAgYWRkQ2lyY2xlcyh+bG5nLCB+bGF0LGNvbG9yID0gfnBhbChjbHVzdDIpKSAlPiUNCiAgb3ZlcmxheVRpdGxlKCJTdG9yZSBDbHVzdGVycyIpICU+JQ0KICBhZGRMZWdlbmQocG9zaXRpb24gPSAiYm90dG9tcmlnaHQiLCB2YWx1ZXMgPSB+Y2x1c3QyLCBwYWwgPSBwYWwpICU+JQ0KICBhZGRNYXJrZXJzKGRhdGEgPSBjbHVzdGVyX2NlbnRyb2lkcywgfm1lYW5fbG5nLCB+bWVhbl9sYXQsIGxhYmVsID0gfmFzLmNoYXJhY3RlcihjbHVzdDIpKQ0KDQptYXANCmBgYA0KDQpOb3cgd2UgaGF2ZSB0aGUgd2FyZWhvdXNlIGxvY2F0aW9ucywgYW5kIHNvbWUgaW5pdGlhbCBjbHVzdGVycy4gSG93ZXZlciwgd2Ugd2lsbCBzZWUgdGhhdCBpbiBzb21lIGNhc2VzIHRoZSBoaWVyYXJjaGljYWwgY2x1c3RlcmluZyBkb2VzIG5vdCByZXN1bHQgaW4gc2Vuc2libGUgYXNzaWdubWVudHMuIEZvciBleGFtcGxlLCB0aGVyZSBhcmUgc2l0dWF0aW9ucyB3aGVyZSBib2RpZXMgb2Ygd2F0ZXIgZXhpc3QgdGhhdCBzaW1wbHkgY2x1c3RlcmluZyBieSBjb29yZGluYXRlcyAobGF0LCBsbmcpIHdpbGwgYmUgdW5hYmxlIHRvIHRha2UgYWNjb3VudCBvZi4NCg0KZS5nLiBpbiB0aGUgYmVsb3csIHdlIGNhbiBzZWUgdGhhdCBhIGxvY2F0aW9uIGlzIG9uIHRoZSBvdGhlciBzaWRlIG9mIHRoZSBlc3R1YXJ5DQpgYGB7cn0NCmNsdXN0ZXJfbnVtYmVyIDwtIDgNCg0Kc2FtcGxlX2NsdXN0ZXIgPC0gbG9jc19kZl9oY19jdXQgJT4lDQogIGZpbHRlcihjbHVzdDIgPT0gY2x1c3Rlcl9udW1iZXIpDQoNCnNhbXBsZV9jZW50cm9pZCA8LSBjbHVzdGVyX2NlbnRyb2lkcyAlPiUNCiAgZmlsdGVyKGNsdXN0MiA9PSBjbHVzdGVyX251bWJlcikNCg0KbWFwIDwtIGxlYWZsZXQoc2FtcGxlX2NsdXN0ZXIpICU+JSBhZGRQcm92aWRlclRpbGVzKHByb3ZpZGVycyRDYXJ0b0RCLlBvc2l0cm9uKSAgJT4lDQogIGFkZENpcmNsZXMofmxuZywgfmxhdCxjb2xvciA9IH5wYWwoY2x1c3QpKSAlPiUNCiAgb3ZlcmxheVRpdGxlKCJTdG9yZSBDbHVzdGVycyIpICU+JQ0KICBhZGRMZWdlbmQocG9zaXRpb24gPSAiYm90dG9tcmlnaHQiLCB2YWx1ZXMgPSB+Y2x1c3QyLCBwYWwgPSBwYWwpICU+JQ0KICBhZGRNYXJrZXJzKGRhdGEgPSBzYW1wbGVfY2VudHJvaWQsIH5tZWFuX2xuZywgfm1lYW5fbGF0KQ0KDQptYXANCmBgYA0KDQpJbiBwcmFjdGljZSB0aGlzIG1lYW5zIHRoYXQgdGhlIGRyaXZlIGRpc3RhbmNlIHJlcXVpcmVkIGZyb20gdGhlIGRlcG90IHRvIHJlc3RvY2sgdGhlIHN0b3JlIGlzIGZhciBncmVhdGVyIHRoYW4gaW5pdGlhbGx5IGFudGljaXBhdGVkLiBGdXJ0aGVybW9yZSwgdGhlcmUgbWF5IGJlIGEgbmVhcmVyIGRlcG90IChiZWxvbmdpbmcgdG8gYW5vdGhlciBjbHVzdGVyKSB0aGF0IGlzIGJldHRlciBzdWl0ZWQgZm9yIHJlc3RvY2tpbmcgdGhpcyBzdG9yZS4NCg0KRm9yIHRoaXMgd2Ugd2lsbCBuZWVkIHRvIHVuZGVyc3RhbmQgdGhlIGRyaXZlIHRpbWUgYW5kIHJlYXNzaWduIGxvY2F0aW9ucyB0byB0aGVpciBuZWFyZXN0IGNsdXN0ZXIgY2VudHJlIGJhc2VkIG9uIHRoaXMuDQoNCmBgYHtyfQ0KIyBjcmVhdGUgYSBjaGVja3BvaW50DQpsb2NzX2RmX2hjX2N1dCA8LSBsb2NzX2RmX2hjX2N1dCAlPiUNCiAgc2VsZWN0KC1jbHVzdCkgJT4lDQogIHJlbmFtZShjbHVzdGVyID0gY2x1c3QyKQ0KDQpjbHVzdGVyX2NlbnRyb2lkcyA8LSBjbHVzdGVyX2NlbnRyb2lkcyAlPiUNCiAgcmVuYW1lKGxhdD1tZWFuX2xhdCwgbG5nPW1lYW5fbG5nLCBjbHVzdGVyID0gY2x1c3QyKQ0KDQp3cml0ZV9yZHMobG9jc19kZl9oY19jdXQsICJkYXRhL2N1dF9zdG9yZV9sb2NzLnJkcyIpDQp3cml0ZV9yZHMoY2x1c3Rlcl9jZW50cm9pZHMsICJkYXRhL3dhcmVob3VzZV9sb2NhdGlvbnMucmRzIikNCmBgYA0KDQoNCg0KDQojIyMgT3B0aW1pc2UgQmFzZWQgb24gRHJpdmUgVGltZQ0KDQpGb3IgdGhpcyBwYXJ0LCB3ZSB3aWxsIGxldmVyYWdlIHRoZSBicmlsbGlhbnQgUHJvamVjdCBPU1JNIChPcGVuIFNvdXJjZSBSb3V0aW5nIE1hY2hpbmUpLCB3aGljaCBjYW4gYmUgZm91bmQgaGVyZTogaHR0cHM6Ly9naXRodWIuY29tL1Byb2plY3QtT1NSTS9vc3JtLWJhY2tlbmQNCg0KSXQgd2lsbCBsZXZlcmFnZSBkb2NrZXIgYW5kIGFsbG93IHVzIHRvIHJ1biBhIGxvY2FsIGFwcCwgd2hpY2ggd2UgY2FuIHNlbmQgQVBJIHJlcXVlc3QgdG8gaW4gb3JkZXIgZGV0ZXJtaW5lIHRoZSBkcml2ZSBkaXN0YW5jZSBhbmQgdGltZSBiZXR3ZWVuIGxvY2F0aW9uIHBhaXJzLg0KDQpGb3IgZnVydGhlciBkZXRhaWxzIG9uIGluc3RhbGxhdGlvbiBhbmQgcnVubmluZyBzZWUgdGhlICJpbnN0YWxsX29zcm1fZG9ja2VyLlJtZCIgZmlsZS4NCg0KYGBge3J9DQpzdG9yZV9sb2NhdGlvbnMgPC0gcmVhZF9yZHMoImRhdGEvY3V0X3N0b3JlX2xvY3MucmRzIikNCndhcmVob3VzZV9sb2NhdGlvbnMgPC0gcmVhZF9yZHMoImRhdGEvd2FyZWhvdXNlX2xvY2F0aW9ucy5yZHMiKQ0KDQojIGNyZWF0ZSB0aGUgc3RyaW5nIG9mIHdhcmVob3VzZSBsb2NhdGlvbnMNCmNvb3JkcyA8LSBjKCkNCmZvciAocm93IGluIDE6bnJvdyh3YXJlaG91c2VfbG9jYXRpb25zKSl7DQogIHdhcmVob3VzZSA8LSBwYXN0ZSh3YXJlaG91c2VfbG9jYXRpb25zJGxuZ1tyb3ddLCB3YXJlaG91c2VfbG9jYXRpb25zJGxhdFtyb3ddLCBzZXA9IiwiKQ0KICBjb29yZHMgPC0gYXBwZW5kKGNvb3Jkcywgd2FyZWhvdXNlKQ0KICANCn0NCndhcmVob3VzZV9zdHJpbmcgPC0gcGFzdGUoY29vcmRzLCBjb2xsYXBzZT0iOyIpDQoNCiMgaXRlcmF0ZSB0aHJvdWdoIGVhY2ggc3RvcmUgYW5kIGZpbmQgdGhlIG5lYXJlc3Qgd2FyZWhvdXNlL2RlcG90DQpjbHVzdGVyX3ZlY3RvciA8LSBjKCkNCmZvciAocm93IGluIDE6bnJvdyhzdG9yZV9sb2NhdGlvbnMpKXsNCiAgbG9jYXRpb24gPC0gc3RvcmVfbG9jYXRpb25zICU+JQ0KICAgIHNlbGVjdChsbmcsIGxhdCkgJT4lDQogICAgc2xpY2Uocm93KSAlPiUNCiAgICBwYXN0ZShjb2xsYXBzZT0iLCIpDQogIA0KICByZXNwb25zZSA8LSBHRVQocGFzdGUwKCJodHRwOi8vMTI3LjAuMC4xOjUwMDAvdGFibGUvdjEvZHJpdmluZy8iLCBsb2NhdGlvbiwgIjsiLCB3YXJlaG91c2Vfc3RyaW5nLCAiP3NvdXJjZXM9MCIpKQ0KICByZXN1bHQgPC0gY29udGVudChyZXNwb25zZSwgYXM9J3BhcnNlZCcpDQogIA0KICBkdXJhdGlvbl9tYXRyaXggPC0gcmVzdWx0JGR1cmF0aW9uc1tbMV1dWy0xXSANCiAgDQogIG5lYXJlc3Rfd2FyZWhvdXNlIDwtIHdhcmVob3VzZV9sb2NhdGlvbnMgJT4lDQogICAgbXV0YXRlKHN0b3JlX3RyYXZlbF9kdXJhdGlvbiA9IGFzLm51bWVyaWMoZHVyYXRpb25fbWF0cml4KSkgJT4lDQogICAgc2xpY2Uod2hpY2gubWluKC4kc3RvcmVfdHJhdmVsX2R1cmF0aW9uKSkgJT4lDQogICAgcHVsbChjbHVzdGVyKQ0KICANCiAgY2x1c3Rlcl92ZWN0b3IgPC0gYXBwZW5kKGNsdXN0ZXJfdmVjdG9yLCBuZWFyZXN0X3dhcmVob3VzZSkNCiAgDQogIGlmIChyb3clJTEwMCA9PSAwKXsNCiAgICBwcmludChwYXN0ZTAocm93LCIvIixucm93KHN0b3JlX2xvY2F0aW9ucykpKQ0KICB9DQogIA0KfQ0KDQpzdG9yZV9sb2NhdGlvbnNfdXBkYXRlZCA8LSBzdG9yZV9sb2NhdGlvbnMgJT4lDQogIG11dGF0ZSh1cGRhdGVkX2NsdXN0ZXIgPSBjbHVzdGVyX3ZlY3RvcikgJT4lDQogIHJlbmFtZShvcmlnaW5hbF9jbHVzdGVyID0gY2x1c3RlcikgJT4lDQogIG11dGF0ZShzYW1lX2NsdXN0ZXIgPSBjYXNlX3doZW4oDQogICAgb3JpZ2luYWxfY2x1c3RlciA9PSB1cGRhdGVkX2NsdXN0ZXIgfiAxLA0KICAgIG9yaWdpbmFsX2NsdXN0ZXIgIT0gdXBkYXRlZF9jbHVzdGVyIH4gMA0KICAgICkpDQoNCg0Kd3JpdGVfcmRzKHN0b3JlX2xvY2F0aW9uc191cGRhdGVkLCAiZGF0YS9tYXN0ZXJfc3RvcmVfZGYucmRzIikNCmBgYA0KDQpOb3cgd2UgY2FuIHJlLXBsb3QgdGhlIHVwZGF0ZWQgY2x1c3RlciBhc3NpZ25tZW50cyB0byBjaGVjayB0aGF0IHByZXZpb3VzIGlzc3VlcyBoYXZlIGJlZW4gYWRkcmVzc2VkDQoNCmBgYHtyfQ0Kc3RvcmVfbG9jYXRpb25zX3VwZGF0ZWQgPC0gcmVhZF9yZHMoImRhdGEvbWFzdGVyX3N0b3JlX2RmLnJkcyIpDQp3YXJlaG91c2VfbG9jYXRpb25zIDwtIHJlYWRfcmRzKCJkYXRhL3dhcmVob3VzZV9sb2NhdGlvbnMucmRzIikNCg0KIyBwcmludCBudW1iZXIgb2YgY2hhbmdlZCBsb2NhdGlvbnMNCnByaW50KGNvdW50KHN0b3JlX2xvY2F0aW9uc191cGRhdGVkLCBzYW1lX2NsdXN0ZXIsIHNvcnQgPSBUUlVFKSkNCg0KIyBwbG90IHRoZSBuZXcgY2x1c3RlcnMNCnBhbCA8LSBjb2xvckZhY3RvcigNCiAgcGFsZXR0ZSA9ICJSZFlsQnUiLA0KICBkb21haW4gPSBzdG9yZV9sb2NhdGlvbnNfdXBkYXRlZCR1cGRhdGVkX2NsdXN0ZXIpDQoNCm1hcCA8LSBsZWFmbGV0KHN0b3JlX2xvY2F0aW9uc191cGRhdGVkKSAlPiUgYWRkUHJvdmlkZXJUaWxlcyhwcm92aWRlcnMkQ2FydG9EQi5Qb3NpdHJvbikgICU+JQ0KICBhZGRDaXJjbGVzKH5sbmcsIH5sYXQsY29sb3IgPSB+cGFsKHVwZGF0ZWRfY2x1c3RlciksIGxhYmVsID0gfmFzLmNoYXJhY3Rlcih1cGRhdGVkX2NsdXN0ZXIpKSAlPiUNCiAgb3ZlcmxheVRpdGxlKCJTdG9yZSBDbHVzdGVycyIpICU+JQ0KICBhZGRMZWdlbmQocG9zaXRpb24gPSAiYm90dG9tcmlnaHQiLCB2YWx1ZXMgPSB+dXBkYXRlZF9jbHVzdGVyLCBwYWwgPSBwYWwpICU+JQ0KICBhZGRNYXJrZXJzKGRhdGEgPSB3YXJlaG91c2VfbG9jYXRpb25zLCB+bG5nLCB+bGF0LCBsYWJlbCA9IH5hcy5jaGFyYWN0ZXIoY2x1c3RlcikpDQoNCm1hcA0KYGBgDQoNCg0KDQojIyMgQXNzaWduaW5nIFByaW1hcnkgdnMgU2Vjb25kYXJ5IFdhcmVob3VzZXMvRGVwb3RzDQoNClRoZSBsYXN0IHRoaW5nIHdlIHdpbGwgZG8gaXMgdG8gZGVjaWRlIHRoZSBhcHByb3ByaWF0ZSBtaXggb2YgbWFpbi9wcmltYXJ5IHdhcmVob3VzZXMsIGFuZCBzYXRlbGxpdGVzL3NlY29uZGFyeSBvbmVzLiBJbiBwcmFjdGljZSBpdCBpcyBvZnRlbiB0aGUgY2FzZSB0aGF0IGEgY29tcGFueSBvcGVyYXRlcyBzZXZlcmFsIG1haW4gZGlzdHJpYnV0aW9ucyBjZW50cmVzLCB3aGljaCBhcmUgdGhlbiByZXNwb25zaWJsZSBmb3Igc2VuZGluZyBzdG9jayB0byBzbWFsbGVyIHdhcmVob3VzZXMgdGhhdCBzZXJ2aWNlIHNwZWNpZmljIGdlb2dyYXBoaWMgcmVnaW9ucy4NCg0KV2Ugd2lsbCByZXBsaWNhdGUgdGhpcyBhcHByb2FjaCBieSBhaW1pbmcgZm9yIGp1c3QgNSBwcmltYXJ5IGRlcG90cywgd2l0aCAxNSBzZWNvbmRhcnkgZGVwb3RzIGZvciBzZXJ2aWNpbmcgdGhlIHJlbWFpbmluZyByZWdpb25zLg0KDQpGb3IgdGhpcyB3ZSB3aWxsIGxvb2sgYXQgdGhlIHN0b3JlIGNvdW50IGFzc2lnbmVkIHRvIGVhY2ggY2x1c3RlciAod2l0aCB0aGUgaWRlYSBiZWluZyB0byBhc3NpZ24gcHJpbWFyeSBkZXBvdHMgYXJlIHRob3NlIHdpdGggbW9zdCBhc3NvY2lhdGVkIHN0b3JlcyksIGFuZCB0aGUgZ2VvZ3JhcGhpYyBzcHJlYWQgKHRvIGVuc3VyZSBvdXIgcHJpbWFyeSBkZXBvdHMgb2ZmZXIgYWRlcXVhdGUgY292ZXJhZ2UgYWNyb3NzIHRoZSBjb3VudHJ5KS4NCg0KVGhpcyBwYXJ0IGlzIGEgc2xpZ2h0bHkgbWFudWFsIHByb2Nlc3MuDQoNCmBgYHtyfQ0KY2x1c3Rlcl9jb3VudCA8LSBzdG9yZV9sb2NhdGlvbnNfdXBkYXRlZCAlPiUNCiAgZ3JvdXBfYnkodXBkYXRlZF9jbHVzdGVyKSAlPiUNCiAgdGFsbHkoKQ0KDQpnZ3Bsb3QoY2x1c3Rlcl9jb3VudCwgYWVzKHggPSByZW9yZGVyKHVwZGF0ZWRfY2x1c3RlciwgLW4pLCB5ID0gbikpICsNCiAgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpICsgdGhlbWVfbWluaW1hbCgpICsNCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSAwLCBoanVzdCA9IDAuNSwgdmp1c3QgPSAwLjMpKQ0KDQpgYGANCg0KYGBge3J9DQp0b3BfNyA8LSBjKDEyLCAzLCAxMSwgMTUsIDEsIDYsIDUsIDIsIDE2KQ0KDQp0b3BfN19jbHVzdGVycyA8LSBzdG9yZV9sb2NhdGlvbnNfdXBkYXRlZCAlPiUNCiAgZmlsdGVyKHVwZGF0ZWRfY2x1c3RlciAlaW4lIHRvcF83KQ0KDQpwYWwgPC0gY29sb3JGYWN0b3IoDQogIHBhbGV0dGUgPSAiUmRZbEJ1IiwNCiAgZG9tYWluID0gdG9wXzdfY2x1c3RlcnMkdXBkYXRlZF9jbHVzdGVyKQ0KDQptYXAgPC0gbGVhZmxldCh0b3BfN19jbHVzdGVycykgJT4lIGFkZFByb3ZpZGVyVGlsZXMocHJvdmlkZXJzJENhcnRvREIuUG9zaXRyb24pICAlPiUNCiAgYWRkQ2lyY2xlcyh+bG5nLCB+bGF0LGNvbG9yID0gfnBhbCh1cGRhdGVkX2NsdXN0ZXIpKSAlPiUNCiAgb3ZlcmxheVRpdGxlKCJTdG9yZSBDbHVzdGVycyIpICU+JQ0KICBhZGRMZWdlbmQocG9zaXRpb24gPSAiYm90dG9tcmlnaHQiLCB2YWx1ZXMgPSB+dXBkYXRlZF9jbHVzdGVyLCBwYWwgPSBwYWwpICU+JQ0KICBhZGRNYXJrZXJzKGRhdGEgPSB3YXJlaG91c2VfbG9jYXRpb25zLCB+bG5nLCB+bGF0LCBsYWJlbCA9IH5hcy5jaGFyYWN0ZXIoY2x1c3RlcikpDQoNCm1hcA0KYGBgDQoNCi0gTG9uZG9uOiBVbnN1cnByaXNpbmdseSBMb25kb24sIHdpdGggc3VjaCBhIGhpZ2ggcG9wdWxhdGlvbiwgY2FwdHVyZXMgdGhlIHR3byBsYXJnZXN0IGNsdXN0ZXJzICgzICYgMTIpLiBHaXZlbiB0aGUgc2l6ZSBvZiB0aGVzZSBjbHVzdGVycywgd2Ugd2lsbCB0YWtlIGJvdGggYXMgcHJpbWFyeSBkZXBvdHMNCi0gTWFuY2hlc3RlcjogVGhlIHRoaXJkIGJpZ2dlc3QgY2x1c3RlciAoMTEpIGlzIGxvY2F0ZWQgaW4gTWFuY2hlc3Rlci4gR2l2ZW4gdGhlIGFiaWxpdHkgdG8gc2VydmljZSB0aGUgc2Vjb25kIGxhcmdlc3QgbnVtYmVyIG9mIHN0b3JlcywgYXMgd2VsbCBhcyB0aGUgbm9ydGhlciByZWdpb25zLCBpdCBpcyByZWFzb25hYmxlIHRvIGFsc28gdGFrZSB0aGlzIGFzIGEgcHJpbWFyeSBkZXBvdC4NCi0gQmlybWluZ2hhbTogVGhlIGRlY2lzaW9uIGZvciB0aGUgbmV4dCBwcmltYXJ5IGRlcG90LCBiZWNvbWVzIGEgd2VpZ2ggdXAgYmV0d2VlbiBjbHVzdGVycyAxICYgMTUuIEhvd2V2ZXIsIGdpdmVuIHRoZSBwcm94aW1pdHkgb2YgY2x1c3RlciAxNSB0byBNYW5jaGVzdGVyLCBJIGNob3NlIHRvIHNlbGVjdCBsb2NhdGlvbiAxIGFzIGEgcHJpbWFyeSBkZXBvdCwgZ2l2ZW4gaXQgY2FuIGJlIHVzZWQgdG8gc2VydmljZSBzZXJ2aWNlIFdhbGVzIGFuZCB0aGUgTWlkbGFuZHMsIHdpdGggdGhlIEVhc3Qgb2YgRW5nbGFuZCBiZWluZyBzZXJ2aWNlZCBvdXQgb2YgTWFuY2hlc3RlciBvciBMb25kb24gYXMgYXBwcm9wcmlhdGUuDQotIEdsYXNnb3c6IExhc3RseSwgaXQgaXMgYWxzbyByZWFzb25hYmxlIHRvIGFzc3VtZSBhIG5vcnRoZXJuIGRlcG90IGluIFNjb3RsYW5kIHRvIHNlcnZpY2UgdGhlIGNvdW50cnksIE5vcnRoIG9mIEVuZ2xhbmQsIGFuZCBOb3J0aGVybiBJcmVsYW5kIHNlY29uZGFyeSBkZXBvdHMuIEdsYXNnb3cgKGNsdXN0ZXIgMTQpIGhhcyB0aGUgaGlnaGVzdCBudW1iZXIgb2Ygc3RvcmVzIGFuZCBpcyB3ZWxsIGxvY2F0ZWQgdG8gc2VydmljZSBOb3J0aGVybiBJcmVsYW5kIG9uIHRoZSB3ZXN0IGNvYXN0Lg0KDQoNCkZyb20gbWFudWFsIGludmVzdGlnYXRpb24sIGFuZCBzb21lIGJ1c2luZXNzIGxvZ2ljIHdlIGhhdmUgYXNzaWduZWQgb3VyIDUgcHJpbWFyeSBkZXBvdHMuIEhvd2V2ZXIsIGl0IGlzIHdvcnRoIG5vdGluZyB0aGF0IHRoaXMgY291bGQgYmUgZG9uZSB1c2luZyBkcml2aW5nIGRpc3RhbmNlIChlLmcuIHBpY2sgNSBsb2NhdGlvbnMgdGhhdCBtaW5pbWlzZSBhZ2dyZWdhdGVkIGRyaXZlIHRpbWUgdG8gcmVhY2ggYWxsIHNlY29uZGFyeSBkZXBvdHMpLCBvciBzb21lIGFsdGVybmF0aXZlIGJ1c2luZXNzIGxvZ2ljLiBUaGlzIHdvdWxkIGJlIGFuIGFyZWEgb2YgcG9zc2libGUgaW1wcm92ZW1lbnQuDQpgYGB7cn0NCiMgcGxvdCB0aGUgZmluYWwgbWFwDQptYXN0ZXJfZGVwb3RzIDwtIGMoMSwgMywgMTEsIDEyLCAxNCkNCg0Kd2FyZWhvdXNlX2xvY2F0aW9ucyA8LSB3YXJlaG91c2VfbG9jYXRpb25zICU+JQ0KICBtdXRhdGUodGllciA9IGlmZWxzZShjbHVzdGVyICVpbiUgbWFzdGVyX2RlcG90cywgJ21hc3RlcicsICdzdWInKSkgJT4lDQogIG11dGF0ZShpY29uX2NvbG91ciA9IGlmZWxzZSh0aWVyID09ICJtYXN0ZXIiLCAicmVkIiwgImJsdWUiKSkNCg0KaWNvbnMgPC0gYXdlc29tZUljb25zKGljb25Db2xvciA9ICJibGFjayIsDQogICAgICAgICAgICAgICAgICAgICAgbGlicmFyeSA9ICJpb24iLA0KICAgICAgICAgICAgICAgICAgICAgIG1hcmtlckNvbG9yID0gd2FyZWhvdXNlX2xvY2F0aW9ucyRpY29uX2NvbG91cg0KICAgICAgICAgICAgICAgICAgICAgICkNCg0KcGFsIDwtIGNvbG9yRmFjdG9yKA0KICBwYWxldHRlID0gIlJkWWxCdSIsDQogIGRvbWFpbiA9IHN0b3JlX2xvY2F0aW9uc191cGRhdGVkJHVwZGF0ZWRfY2x1c3RlcikNCg0KbWFwIDwtIGxlYWZsZXQoc3RvcmVfbG9jYXRpb25zX3VwZGF0ZWQpICU+JSBhZGRQcm92aWRlclRpbGVzKHByb3ZpZGVycyRDYXJ0b0RCLlBvc2l0cm9uKSAgJT4lDQogIGFkZENpcmNsZXMofmxuZywgfmxhdCxjb2xvciA9IH5wYWwodXBkYXRlZF9jbHVzdGVyKSkgJT4lDQogIG92ZXJsYXlUaXRsZSgiU3RvcmUgQ2x1c3RlcnMiKSAlPiUNCiAgYWRkTGVnZW5kKHBvc2l0aW9uID0gImJvdHRvbXJpZ2h0IiwgdmFsdWVzID0gfnVwZGF0ZWRfY2x1c3RlciwgcGFsID0gcGFsKSAlPiUNCiAgYWRkQXdlc29tZU1hcmtlcnMoZGF0YSA9IHdhcmVob3VzZV9sb2NhdGlvbnMsIH5sbmcsIH5sYXQsIGljb24gPSBpY29ucywgbGFiZWwgPSB+YXMuY2hhcmFjdGVyKGNsdXN0ZXIpKQ0KDQptYXANCmBgYA0KDQoNCiMjIyBQb3RlbnRpYWwgSW1wcm92ZW1lbnRzDQoNCiMjIyMgVmVoaWNsZSBSb3V0aW5nOg0KVGhlIG5leHQgc3RlcCB3b3VsZCBiZSB0byBkZXRlcm1pbmUgdGhlIG1vc3Qgb3B0aW1hbCBkcml2aW5nIHJvdXRlcyBmb3IgdHJ1Y2tzIHRvIGxlYXZlIGVhY2ggb2YgdGhlIHByaW1hcnkgZGVwb3RzIHRvIHNlcnZpY2UgdGhlIHNlY29uZGFyeSBkZXBvdHMgLSBpLmUgd2hhdCBpcyB0aGUgbW9zdCBlZmZpY2llbnQgbWV0aG9kIG9mIHRydWNrcywgbGVhdmluZyBlYWNoIG9mIHRoZSB0aGUgcHJpbWFyeSBkZXBvdHMsIHRvIHNlcnZpY2UgdGhlIHNlY29uZGFyeSBkZXBvdHMgaW4gb3VyIG5ldHdvcmsuIA0KDQpJZiB3ZSBhc3N1bWUsIGdpdmVuIHRoZSBzaXplIG9mIGRlbGl2ZXJpZXMsIGl0IGlzIHJlYWxpc3RpYyB0byBoYXZlIGEgc2luZ2xlIHRydWNrIHBlciBzZWNvbmRhcnkgZGVwb3QgcmVzdG9jaywgdGhlIHByb2JsZW0gYmVjb21lcyBzaW1wbHkgYSBzaG9ydGVzdCBkdXJhdGlvbiBjYWxjdWxhdGlvbiBmcm9tIGVhY2ggc2Vjb25kYXJ5IGRlcG90IHRvIHRoZSBwcmltYXJ5IGRlcG90cyAtIHdoaWNoIE9TUk0gZG9ja2VyIGNhbiBoYW5kbGUuDQoNCkhvd2V2ZXIsIGlmIHlvdSB3YW50IGp1c3Qgb25lIHRydWNrIHBlciBkZXBvdCB0byBiZSByZXNwb25zaWJsZSBmb3IgcmVzdG9ja2luZyBlYWNoIG9mIHRoZSBhc3NvY2lhdGVkIHNlY29uZGFyeSBkZXBvdHMsIHRoZW4gd2UgbmVlZCB0byBmcmFtZSB0aGUgcHJvYmxlbSBhcyBhICJ0cmF2ZWxsaW5nIHNhbGVzbWFuIHByb2JsZW0iIC0gaS5lLiB3aGF0IGlzIHRoZSBzaG9ydGVzdCByb3V0ZSBmb3IgYSBzaW5nbGUgdHJ1Y2sgdG8gdmlzaXQgYWxsIHRoZSBuZWNlc3Nhcnkgc3RvcHMganVzdCBvbmNlIGFuZCByZXR1cm4gaG9tZSB0byB0aGUgcHJpbWFyeSBkZXBvdD8gRm9yIHRoaXMsIEkgd291bGQgcmVjb21tZW5kIGV4cGxvcmluZyB0aGUgVlJPT00gUHJvamVjdCAoVmVoaWNsZSBSb3V0aW5nIE9wZW4tU291cmNlIE9wdGltaXNhdGlvbiBNYWNoaW5lKTogaHR0cHM6Ly9naXRodWIuY29tL1ZST09NLVByb2plY3QgDQoNCg0KIyMjIyBQcmltYXJ5IERlcG90IFNlbGVjdGlvbjoNCkFzIG1lbnRpb25lZCBhYm92ZSwgYW5vdGhlciBwb3RlbnRpYWwgaW1wcm92ZW1lbnQgY291bGQgYmUgdG8gc2VsZWN0IHRoZSBsb2NhdGlvbiBvZiB0aGUgcHJpbWFyeSBkZXBvdHMsIGJhc2VkIG5vdCBvbiB0aGUgbnVtYmVyIG9mIGxvY2F0aW9ucyBhbmQgbWFudWFsIGJ1c2luZXNzIGxvZ2ljLCBidXQgYmFzZWQgb24gd2hpY2ggNSBsb2NhdGlvbnMgbWluaW1pc2UgdGhlIGRyaXZlIHRpbWUgdG8gZWFjaCBvZiB0aGUgcmVtYWluaW5nIHNlY29uZGFyeSBkZXBvdHMuIEZvciB0aGlzIHdlIGNvdWxkIHVzZSBPU1JNIGRvY2tlciBhbHNvLg0KDQoNCiMjIyMgQ2x1c3RlciBQcnVuaW5nOg0KV2hpbGUgZWFybGllciBvbiB3ZSBkaXNyZWdhcmRlZCBjbHVzdGVycyB0aGF0IHdlcmUgZXh0cmVtZWx5IHJlbW90ZSwgdGhlcmUgYXJlIHN0aWxsIGEgZmV3IGxvY2F0aW9ucyBjb250YWluZWQgaW4gb3VyIGNsdXN0ZXJzIHRoYXQgYXJlIHJhdGhlciByZW1vdGUsIHdpdGggdGhlIGNsdXN0ZXIgYmVpbmcgZmFpcmx5IHNwcmVhZCAoZS5nLiBOb3J0aCBFYXN0IFNjb3RsYW5kKS4gDQoNClRvIGFkZHJlc3MgdGhpcywgd2UgY291bGQgaXRlcmF0aXZlbHkgcmVtb3ZlIGxvY2F0aW9ucyBmcm9tIG91ciBkYXRhc2V0IHRoYXQgYXJlIGEgY2VydGFpbiBkaXN0YW5jZSBmcm9tIHRoZSBuZWFyZXN0IGNsdXN0ZXIgY2VudHJlLCBhbmQgdGhlbiBwZXJmb3JtIHJlY2x1c3RlcmluZy4gVGhlIGludGVudGlvbiB3b3VsZCBiZSB0byByZXBlYXRlZGx5ICJwcnVuZSIgdGhlIG1vc3QgcmVtb3RlIGxvY2F0aW9ucyBpbiBiZXR3ZWVuIHJvdW5kcyBvZiBjbHVzdGVyaW5nLg0KDQpUaGlzIHdvdWxkIHJlc3VsdCBpbiBtb3JlIGRlbnNlbHkgZm9ybWVkIHJlZ2lvbnMsIGJ1dCBhdCB0aGUgZXhwZW5zZSBvZiBleGNsdWRpbmcgY2VydGFpbiBsb2NhdGlvbnMgZnJvbSBvdXIgbG9naXN0aWNzIG5ldHdvcmsuDQoNCg0KDQojIyMgQ2xvc2luZyBDb21tZW50cw0KVGhlcmUgd2UgaGF2ZSBpdCwgb3VyIGNsdXN0ZXJlZCBzdG9yZSB0ZXJyaXRvcmllcywgd2l0aCB0aGUgcHJvcG9zZWQgbG9jYXRpb24gb2YgZWFjaCB3YXJlaG91c2UgYW5kIGEgcHJvcG9zZWQgdmlldyBvZiB3aGljaCBzaG91bGQgbWUgbWFqb3IgZGVwb3RzLCBhbmQgd2hpY2ggc2hvdWxkIGJlIHNlY29uZGFyeSBkZXBvdHMuDQoNCldoaWxlIHRoZXJlIGFyZSBhIGZldyBhcmVhcyBvZiBwb3RlbnRpYWwgaW1wcm92ZW1lbnQgbm90ZWQgYWJvdmUsIHRoaXMgcHJvamVjdCBpbnRyb2R1Y2VzIGFuZCBpbml0aWFsIGFwcHJvYWNoIHRoYXQgY2FuIGJlIHRha2VuIHRvIG9wdGltaXNlIGxvZ2lzdGljcyBuZXR3b3JrcyB1c2luZyBnZW9zcGF0aWFsIGRhdGEuIFRoZSB0b29scyBhbmQgdGVjaG5pcXVlcyBpbnRyb2R1Y2VkIGNhbiBiZSB1c2VkIGluIHRoZWlyIGN1cnJlbnQgZm9ybSwgb3IgdGFrZSBmdXJ0aGVyIHdpdGggbW9yIGNvbXBsZXggYXBwcm9hY2hlcywgdG8gaGVscCBjb21wYW5pZXMgZ2FpbiBhY3Rpb25hYmxlIGluc2lnaHRzIGludG8gdGhlaXIgbG9naXN0aWNzLCBtYXJrZXRpbmcsIG9yIGFjcXVpc2l0aW9uIGFwcHJvYWNoZXMgLSBhbmQgZ2FpbiBhIGNvbXBldGl0aXZlIGVkZ2UgaW4gdGhlaXIgcmVzcGVjdGl2ZSBtYXJrZXQu